Low-Level Design (LLD) questions test your ability to translate real-world requirements into clean, extensible code. Unlike system design (which focuses on databases, caching, and scalability), LLD focuses on object-oriented design, design patterns, and good architecture. LLD questions appear in senior engineering interviews at Google, Amazon, Meta, and Apple. If you can write clean LLD code, you stand out. This guide walks you through the most common LLD problems with complete solutions.
LLD vs. System Design
System Design (HLD): How would you design YouTube? Focus on databases, scalability, distribution.
Low-Level Design (LLD): How would you design a parking lot system? Focus on classes, methods, design patterns, and clean code.
LLD questions are about writing code that's extensible, maintainable, and follows SOLID principles.
Key Design Patterns for LLD
Before diving into specific problems, understand these patterns:
Strategy Pattern: Different algorithms implementing the same interface.
Factory Pattern: Create objects without specifying exact classes.
Observer Pattern: Objects notify others about state changes.
Singleton Pattern: Exactly one instance of a class.
Adapter Pattern: Make incompatible interfaces work together.
Decorator Pattern: Add functionality to objects dynamically.
Problem 1: Design a Parking Lot
Requirements:
- Parking lot has multiple levels, each with multiple spots
- Spots have different sizes: compact, regular, large
- Vehicles have different sizes: motorcycle, car, truck
- Users can park, unpark, and check available spots
- System tracks occupied and available spots
Design:
from enum import Enum
from datetime import datetime
from typing import Optional
class VehicleSize(Enum):
MOTORCYCLE = 1
CAR = 2
TRUCK = 3
class ParkingSpotSize(Enum):
COMPACT = 1
REGULAR = 2
LARGE = 3
class Vehicle:
def __init__(self, license_plate: str, size: VehicleSize):
self.license_plate = license_plate
self.size = size
class ParkingSpot:
def __init__(self, level: int, spot_number: int, size: ParkingSpotSize):
self.level = level
self.spot_number = spot_number
self.size = size
self.is_available = True
self.vehicle: Optional[Vehicle] = None
def park(self, vehicle: Vehicle) -> bool:
if not self.is_available or not self._can_fit_vehicle(vehicle):
return False
self.is_available = False
self.vehicle = vehicle
return True
def unpark(self) -> Vehicle:
vehicle = self.vehicle
self.is_available = True
self.vehicle = None
return vehicle
def _can_fit_vehicle(self, vehicle: Vehicle) -> bool:
if self.size == ParkingSpotSize.COMPACT:
return vehicle.size == VehicleSize.MOTORCYCLE
elif self.size == ParkingSpotSize.REGULAR:
return vehicle.size in [VehicleSize.MOTORCYCLE, VehicleSize.CAR]
else: # LARGE
return True
class ParkingLevel:
def __init__(self, level: int, num_compact: int, num_regular: int, num_large: int):
self.level = level
self.spots = []
spot_number = 0
for _ in range(num_compact):
self.spots.append(ParkingSpot(level, spot_number, ParkingSpotSize.COMPACT))
spot_number += 1
for _ in range(num_regular):
self.spots.append(ParkingSpot(level, spot_number, ParkingSpotSize.REGULAR))
spot_number += 1
for _ in range(num_large):
self.spots.append(ParkingSpot(level, spot_number, ParkingSpotSize.LARGE))
spot_number += 1
def park(self, vehicle: Vehicle) -> bool:
for spot in self.spots:
if spot.park(vehicle):
return True
return False
def unpark(self, vehicle: Vehicle) -> bool:
for spot in self.spots:
if spot.vehicle == vehicle:
spot.unpark()
return True
return False
def available_spots_by_size(self, size: ParkingSpotSize) -> int:
return sum(1 for spot in self.spots if spot.is_available and spot.size == size)
class ParkingLot:
def __init__(self, num_levels: int, spots_per_level_config: list):
self.levels = []
for i in range(num_levels):
config = spots_per_level_config[i]
self.levels.append(ParkingLevel(i, config['compact'], config['regular'], config['large']))
def park(self, vehicle: Vehicle) -> bool:
for level in self.levels:
if level.park(vehicle):
return True
return False
def unpark(self, vehicle: Vehicle) -> bool:
for level in self.levels:
if level.unpark(vehicle):
return True
return False
def get_available_spots(self) -> dict:
spots = {'compact': 0, 'regular': 0, 'large': 0}
for level in self.levels:
spots['compact'] += level.available_spots_by_size(ParkingSpotSize.COMPACT)
spots['regular'] += level.available_spots_by_size(ParkingSpotSize.REGULAR)
spots['large'] += level.available_spots_by_size(ParkingSpotSize.LARGE)
return spotsKey design decisions:
- Vehicle and ParkingSpot are separate classes
- ParkingLevel manages multiple spots
- ParkingLot manages multiple levels
- Encapsulation: each class knows only what it needs
- Easy to extend: add new vehicle sizes or spot types
Problem 2: Design an Elevator System
Requirements:
- Multiple elevators in a building
- Elevators can move up/down
- Users can request elevators from different floors
- System should efficiently assign elevators
Design:
from enum import Enum
from collections import deque
class Direction(Enum):
UP = 1
DOWN = -1
IDLE = 0
class Request:
def __init__(self, floor: int):
self.floor = floor
class Elevator:
def __init__(self, elevator_id: int, max_floor: int):
self.elevator_id = elevator_id
self.current_floor = 0
self.max_floor = max_floor
self.direction = Direction.IDLE
self.requests = deque()
def add_request(self, request: Request):
if 0 <= request.floor <= self.max_floor and request.floor != self.current_floor:
self.requests.append(request)
self._update_direction()
def _update_direction(self):
if not self.requests:
self.direction = Direction.IDLE
return
next_floor = self.requests[0].floor
if next_floor > self.current_floor:
self.direction = Direction.UP
elif next_floor < self.current_floor:
self.direction = Direction.DOWN
def move(self):
if self.direction == Direction.IDLE:
return
self.current_floor += self.direction.value
# Check if we've reached a requested floor
if self.requests and self.requests[0].floor == self.current_floor:
self.requests.popleft()
self._update_direction()
def is_moving(self) -> bool:
return self.direction != Direction.IDLE
class ElevatorSystem:
def __init__(self, num_elevators: int, max_floor: int):
self.elevators = [Elevator(i, max_floor) for i in range(num_elevators)]
self.max_floor = max_floor
def request_elevator(self, floor: int):
elevator = self._find_best_elevator(floor)
if elevator:
elevator.add_request(Request(floor))
def _find_best_elevator(self, floor: int) -> Optional[Elevator]:
# Closest idle elevator, or closest moving elevator
idle_elevators = [e for e in self.elevators if not e.is_moving()]
if idle_elevators:
return min(idle_elevators, key=lambda e: abs(e.current_floor - floor))
# All elevators moving; pick closest
return min(self.elevators, key=lambda e: abs(e.current_floor - floor))
def step(self):
# Simulate time step
for elevator in self.elevators:
elevator.move()Key design decisions:
- Direction enum represents elevator state
- Elevator maintains queue of requests
- ElevatorSystem chooses best elevator for each request
- Extensible: easy to add new assignment strategies
Problem 3: Design a Vending Machine
Requirements:
- Users select items and insert money
- System dispenses items if paid enough
- System returns change
- Track inventory
Design:
from enum import Enum
class Item:
def __init__(self, name: str, price: float, quantity: int):
self.name = name
self.price = price
self.quantity = quantity
class Coin(Enum):
PENNY = 0.01
NICKEL = 0.05
DIME = 0.10
QUARTER = 0.25
class VendingMachine:
def __init__(self):
self.inventory = {}
self.money_inserted = 0
self.coins = {}
def add_item(self, location: str, item: Item, quantity: int):
if location not in self.inventory:
self.inventory[location] = item
self.inventory[location].quantity += quantity
def insert_coin(self, coin: Coin):
self.money_inserted += coin.value
self.coins[coin] = self.coins.get(coin, 0) + 1
def select_item(self, location: str) -> str:
if location not in self.inventory:
return "Invalid location"
item = self.inventory[location]
if item.quantity == 0:
return "Out of stock"
if self.money_inserted < item.price:
return f"Insufficient funds. Need ${item.price:.2f}, have ${self.money_inserted:.2f}"
# Dispense item
item.quantity -= 1
change = self.money_inserted - item.price
self.money_inserted = 0
return f"Dispensed {item.name}. Change: ${change:.2f}"
def return_money(self) -> float:
change = self.money_inserted
self.money_inserted = 0
self.coins = {}
return changeProblem 4: Design a Library Management System
Requirements:
- Books can be borrowed and returned
- Users have a borrowing limit
- Track due dates and fines
- Search for books by title or author
Design:
from datetime import datetime, timedelta
from typing import List
class User:
def __init__(self, user_id: str, name: str):
self.user_id = user_id
self.name = name
self.borrowed_books = []
self.max_borrow_limit = 5
def can_borrow(self) -> bool:
return len(self.borrowed_books) < self.max_borrow_limit
def borrow(self, book: 'Book') -> bool:
if not self.can_borrow():
return False
self.borrowed_books.append(book)
return True
def return_book(self, book: 'Book') -> bool:
if book in self.borrowed_books:
self.borrowed_books.remove(book)
return True
return False
class Book:
def __init__(self, isbn: str, title: str, author: str):
self.isbn = isbn
self.title = title
self.author = author
self.is_available = True
self.borrowed_by: Optional[User] = None
self.due_date: Optional[datetime] = None
def borrow(self, user: User) -> bool:
if not self.is_available:
return False
self.is_available = False
self.borrowed_by = user
self.due_date = datetime.now() + timedelta(days=14)
return user.borrow(self)
def return_book(self) -> float:
fine = self._calculate_fine()
self.is_available = True
self.borrowed_by = None
self.due_date = None
return fine
def _calculate_fine(self) -> float:
if datetime.now() <= self.due_date:
return 0.0
days_late = (datetime.now() - self.due_date).days
return days_late * 0.5 # 50 cents per day
class Library:
def __init__(self):
self.books = {}
self.users = {}
def add_book(self, book: Book):
if book.isbn not in self.books:
self.books[book.isbn] = []
self.books[book.isbn].append(book)
def search_by_title(self, title: str) -> List[Book]:
return [b for books in self.books.values() for b in books if title.lower() in b.title.lower()]
def search_by_author(self, author: str) -> List[Book]:
return [b for books in self.books.values() for b in books if author.lower() in b.author.lower()]
def borrow_book(self, user_id: str, isbn: str) -> bool:
user = self.users.get(user_id)
if not user or isbn not in self.books:
return False
for book in self.books[isbn]:
if book.is_available and book.borrow(user):
return True
return False
def return_book(self, user_id: str, isbn: str) -> float:
user = self.users.get(user_id)
if not user:
return -1
for book in user.borrowed_books:
if book.isbn == isbn:
user.return_book(book)
fine = book.return_book()
return fine
return -1Common LLD Mistakes
Mistake 1: Poor encapsulation Exposing internal details; let classes manage their own state.
Mistake 2: God objects One class doing too much. Distribute responsibilities.
Mistake 3: Ignoring SOLID principles S: Single responsibility, O: Open/closed, L: Liskov substitution, I: Interface segregation, D: Dependency inversion.
Mistake 4: No extensibility Hard-code behavior instead of using patterns that allow easy extension.
Mistake 5: Mixing concerns Don't mix business logic with I/O or persistence.
LLD Interview Tips
- Clarify requirements. Ask about edge cases and constraints.
- Design before coding. Sketch classes and relationships on paper/whiteboard.
- Use design patterns. They show you understand good design.
- Write clean code. Use proper naming, methods, and encapsulation.
- Think extensibility. "What if we add a new requirement?"
- Handle edge cases. Empty lists, null values, invalid inputs.
- Test your design. Walk through scenarios mentally.
Practicing LLD Effectively
- Solve 15-20 LLD problems
- For each, design before coding
- Code cleanly with proper OOP
- Consider scalability and extensibility
- Refactor after solving
Preparing for LLD Interviews
LLD interviews test whether you can write production-quality code. When you're practicing LLD design and want feedback on your object-oriented architecture, Phantom Code can help by providing design suggestions and catching architectural issues in real-time through its chat interface. While it's primarily designed for algorithmic interviews, the underlying technology can help with code quality feedback. Plans start at ₹499/month at phantomcode.co.
Final Thoughts
Low-Level Design is about writing code that's clean, extensible, and follows best practices. Master the problems in this guide and you'll be ready for LLD interviews at top tech companies.