Low-Level Design Interview Questions — A Complete Walkthrough
Who this is for: Anyone preparing for low-level design interviews — or anyone who wants to see the OOP, SOLID, and design-pattern concepts from the previous documents come together into real, working designs. We’ll walk through six classic problems end-to-end.
Table of Contents
- How to Approach Any LLD Interview Problem
- The Universal Framework
- Common Pitfalls in LLD Interviews
- Problem 1 — Design a Parking Lot
- Problem 2 — Design an Elevator System
- Problem 3 — Design a Library Management System
- Problem 4 — Design a Vending Machine
- Problem 5 — Design a Splitwise-Like Expense Sharing System
- Problem 6 — Design a Ride-Hailing System (Uber-Lite)
- Meta-Patterns Across All Six Problems
- Final Interview Tips
1. How to Approach Any LLD Interview Problem
The LLD interview isn’t really about the specific problem — parking lot, elevator, chess, whatever. It’s about demonstrating a structured thinking process. The interviewer watches how you decompose ambiguity, handle tradeoffs, and communicate design decisions.
Most candidates fail not because they can’t code, but because they:
- Dive into writing classes before clarifying requirements.
- Design for imagined scope instead of actual scope.
- Can’t explain why they chose a particular class structure.
- Miss the chance to apply SOLID principles or known patterns when they fit naturally.
We’ll beat all of that by following a consistent framework for every problem.
The Mental Model
An LLD interview is a conversation, not a monologue. Think of yourself as a design consultant: the interviewer is your client, they’ve given you vague requirements, and your job is to produce a clean, extensible design while thinking out loud.
You will be judged on:
- Clarity of thought — Can you reduce ambiguity into specific decisions?
- Object identification — Can you find the right classes (not too many, not too few)?
- Relationships — Do your classes compose in ways that make sense (is-a vs has-a)?
- SOLID awareness — Are you subtly baking in extensibility?
- Pattern fluency — Do you reach for patterns when genuinely appropriate?
- Communication — Can you explain your decisions and weigh tradeoffs?
Writing perfect code is not the primary criterion. Writing thoughtful, defensible design is.
2. The Universal Framework
Apply these six steps to every LLD problem. Internalize this; it’s your skeleton for the whole interview.
Step 1 — Clarify Requirements (5-10 min)
Never assume. Ask clarifying questions to define scope. Categorize them:
Functional requirements — what the system must do.
- What are the core actions the system supports?
- Who are the users/actors?
- What are the happy-path scenarios?
- What edge cases exist?
Non-functional requirements — qualities of the system.
- Scale? (How many users, items, events/sec?)
- Concurrency? (Single-threaded vs multi-threaded?)
- Persistence? (In-memory vs database?)
- Real-time constraints?
Out of scope — explicitly state what you won’t design.
- Payment gateway internals? No.
- Frontend UI? No.
- Authentication? Probably no.
State these back to the interviewer: “So I’ll focus on X, Y, Z, and assume A, B are out of scope — does that work?”
Step 2 — Identify Core Entities (Nouns)
Read your requirements. Pull out the nouns. These are your candidate classes.
Parking Lot → Lot, Floor, Spot, Vehicle, Ticket, Payment, Gate, User. Library → Book, Member, Loan, Librarian, Shelf, Reservation.
Not every noun becomes a class. Filter:
- Distinct identity? A “parking spot” has identity; a “free space” doesn’t.
- Distinct behavior or state? If two “nouns” behave identically, collapse them.
- Meaningful in the domain? Words like “process,” “system,” “data” are usually not classes.
Step 3 — Identify Actions (Verbs) and Actors
What are the use cases? For each use case, identify:
- Actor — who initiates it (Customer, Admin, System).
- Action — what happens (Park a car, Issue a book).
- Collaborators — which entities participate.
This shapes your methods. Actions like “park a car” become methods on ParkingLot or ParkingSpot. Methods follow the entities they logically belong to.
Step 4 — Define Relationships
For each pair of related classes, ask:
- Is-a (inheritance)?
Caris-aVehicle. - Has-a (composition)?
ParkingLothas-a collection ofFloors. - Uses-a (dependency)?
PaymentProcessoruses aPricingStrategy.
Prefer composition over inheritance — this avoids rigid hierarchies (remember the “Favor composition over inheritance” principle from the OOP doc).
Multiplicities matter:
- One-to-one: a
Ticketbelongs to oneVehicle. - One-to-many: a
Floorhas manySpots. - Many-to-many:
Memberscan have manyBooks,Bookscan be read by many members over time.
Step 5 — Apply SOLID and Patterns
Now, with the skeleton in place, look for opportunities:
- Strategy — is there an algorithm that might vary? (Pricing, discount, matching.)
- State — does an entity have a lifecycle with distinct behaviors? (Elevator, order, vehicle in lot.)
- Observer — is there a “when X changes, notify Y” relationship? (Display boards, notifications.)
- Factory — is object creation complex or variable? (Different vehicle types, different spot types.)
- Singleton — genuinely one-of-a-kind resource? (Pool manager, coordinator — but be wary of overuse.)
- Chain of Responsibility — escalation or ordered handling? (Approvals, matching priorities.)
- Command — actions that need to be queued, undone, or logged? (Bookings, transactions.)
You don’t have to force patterns. If none apply, that’s fine — simple code is often best.
Check SOLID:
- SRP — does each class have one reason to change?
- OCP — can I add a new variant without modifying existing classes?
- LSP — do subclasses honor their parent’s contract?
- ISP — are interfaces small and focused?
- DIP — are high-level classes depending on abstractions?
Step 6 — Write Code (The Design You’ve Designed)
Now, only now, write code. If you’ve done steps 1-5 well, this part is fast and clean. You’re transcribing, not inventing.
Use:
- Python (or whatever language the interviewer prefers).
- Clear class and method names.
- Type hints (shows discipline).
- Docstrings for classes and non-obvious methods.
@dataclasswhere appropriate to cut boilerplate.- Abstract base classes (
abc) for contracts. - Short working examples showing the API in action.
Resist the urge to implement everything. Sketch the key classes; leave simple methods as skeletons. An interview is about design, not LeetCode.
Step 7 — Discuss Extensions & Tradeoffs
Great candidates anticipate follow-ups. Proactively discuss:
- “What if we needed to add X?”
- “This design assumes single-threaded; here’s how we’d handle concurrency.”
- “We could swap this strategy for that one easily because of X.”
- “If scale grew to 1M events/sec, this in-memory map wouldn’t work — we’d need Y.”
This shows design maturity. You’re not just solving the problem; you’re owning it.
3. Common Pitfalls
Avoid these recurring mistakes:
Pitfall 1 — Jumping to Code Too Early
You hear “parking lot” and start writing class ParkingLot:. Don’t. Spend 5-10 minutes clarifying first. Interviewers expect this — if you skip it, you’re signaling poor process.
Pitfall 2 — Over-Engineering
A parking lot for an interview doesn’t need a Visitor pattern for pricing strategies combined with an Abstract Factory for spot types. Match complexity to the problem. Start simple; add sophistication only where justified.
Pitfall 3 — Under-Engineering
The opposite: one giant ParkingLot class with 40 methods. No inheritance, no strategy for pricing, no separation of concerns. This fails every SOLID test and interviewers notice immediately.
Pitfall 4 — Ignoring the Interviewer
The interviewer drops hints. “What if the parking lot had multiple floors?” isn’t a casual question — they want you to account for it. Listen actively and adjust your design.
Pitfall 5 — Silence
Think out loud. “I’m considering making Vehicle abstract with Car/Bike/Truck subclasses — the alternative would be a single class with a type enum. I’ll go with inheritance because the pricing and spot-size logic differs genuinely per type.” That narration is half the evaluation.
Pitfall 6 — Not Validating the Design
After coding, run a mental walkthrough: “A customer enters with a car. They grab a ticket. We find a free spot. They park. Later they pay and leave.” Catch any gaps live.
Pitfall 7 — Forgetting Edge Cases
- What if the lot is full?
- What if a user loses their ticket?
- What if payment fails?
- What if the elevator is called on two floors at once?
Even if you don’t implement every edge case, mentioning them shows maturity.
4. Problem 1 — Design a Parking Lot
The Prompt: Design the object model for a multi-floor parking lot system.
This is perhaps the most-asked LLD question in existence. Let’s walk through it by-the-book.
Step 1 — Clarify Requirements
Questions to ask:
- Multiple floors? → Yes (assume 3-5 floors).
- Different vehicle types? → Yes: Bike, Car, Truck.
- Different spot sizes? → Yes: Small (bikes), Medium (cars), Large (trucks).
- Pricing model? → Hourly. First hour flat; subsequent hours variable.
- Entry/exit gates? → Multiple of each.
- Payments? → Assume cash + card, but payment gateway integration is out of scope.
- Display boards showing free spots? → Yes, per floor.
- Concurrency? → Assume single-threaded for simplicity; mention locking in extensions.
- Persistence? → In-memory.
Scope summary: “I’ll design a multi-floor lot with typed vehicles, typed spots, entry/exit flow, ticket issuance, hourly billing with pluggable pricing, and display boards that show real-time availability. Concurrency handling and database persistence are out of scope.”
Step 2 — Identify Entities (Nouns)
From the requirements:
ParkingLot— top-level entity.Floor— a level of the lot.ParkingSpot— a specific spot (sized: small/medium/large).Vehicle— abstract;Bike,Car,Truckare concrete.Ticket— issued on entry; used to compute billing.EntryGate,ExitGate— gates (likely just one class with a role).Payment— records payment for a ticket.DisplayBoard— shows availability per floor.PricingStrategy— pluggable billing algorithm.
Reject: “customer” (they’re just an external actor), “system” (too vague), “data” (not a thing).
Step 3 — Identify Actions
- Park a vehicle: actor = driver + entry gate; produces a ticket.
- Exit: actor = driver + exit gate; consumes ticket, computes bill, accepts payment, frees the spot.
- View availability: actor = display board consuming floor state.
Step 4 — Relationships
ParkingLothas-manyFloors(composition).Floorhas-manyParkingSpots(composition).Vehicleis the abstract parent;Bike/Car/Truckare concrete subclasses (is-a).ParkingSpotis associated with aSpotSizeenum or subtypes.Ticketreferences oneVehicleand oneParkingSpot.ExitGateuses aPricingStrategy(dependency injection).DisplayBoardobserves itsFloor(Observer pattern).
Step 5 — SOLID and Patterns
- Strategy —
PricingStrategyis pluggable (hourly, flat-rate, weekend-special). - Observer —
DisplayBoardwatchesFloorchanges. When a spot fills/empties, the board updates automatically. - Factory (optional) —
VehicleFactorycould create vehicles from input, but we can skip this for simplicity. - Singleton —
ParkingLotis sometimes modeled as a singleton. We’ll avoid this unless justified (prefer dependency injection). - SRP — pricing lives in the strategy, not the gate; display logic in display board, not floor.
- OCP — new vehicle types, new spot types, new pricing models: add classes, don’t modify.
Step 6 — Code
from abc import ABC, abstractmethod
from enum import Enum
from datetime import datetime
from typing import Optional
import uuid
# --- Enums ---
class VehicleType(Enum):
BIKE = "bike"
CAR = "car"
TRUCK = "truck"
class SpotSize(Enum):
SMALL = 1 # fits Bike
MEDIUM = 2 # fits Bike, Car
LARGE = 3 # fits Bike, Car, Truck
# --- Vehicle hierarchy (inheritance for type-specific behavior) ---
class Vehicle(ABC):
def __init__(self, license_plate: str):
self.license_plate = license_plate
@property
@abstractmethod
def type(self) -> VehicleType: pass
@property
@abstractmethod
def required_size(self) -> SpotSize: pass
class Bike(Vehicle):
@property
def type(self): return VehicleType.BIKE
@property
def required_size(self): return SpotSize.SMALL
class Car(Vehicle):
@property
def type(self): return VehicleType.CAR
@property
def required_size(self): return SpotSize.MEDIUM
class Truck(Vehicle):
@property
def type(self): return VehicleType.TRUCK
@property
def required_size(self): return SpotSize.LARGE
# --- Parking spot ---
class ParkingSpot:
def __init__(self, spot_id: str, size: SpotSize):
self.spot_id = spot_id
self.size = size
self.vehicle: Optional[Vehicle] = None
@property
def is_free(self) -> bool:
return self.vehicle is None
def can_fit(self, vehicle: Vehicle) -> bool:
# A spot can fit a vehicle if it's at least as large as the vehicle needs
return self.size.value >= vehicle.required_size.value
def park(self, vehicle: Vehicle):
if not self.is_free:
raise ValueError(f"Spot {self.spot_id} is occupied")
if not self.can_fit(vehicle):
raise ValueError(f"Vehicle doesn't fit in spot {self.spot_id}")
self.vehicle = vehicle
def vacate(self):
self.vehicle = None
# --- Observer pattern for display boards ---
class FloorObserver(ABC):
@abstractmethod
def update(self, floor): pass
class Floor:
def __init__(self, floor_number: int, spots: list[ParkingSpot]):
self.floor_number = floor_number
self.spots = spots
self._observers: list[FloorObserver] = []
def attach(self, observer: FloorObserver):
self._observers.append(observer)
def _notify(self):
for o in self._observers:
o.update(self)
def find_free_spot(self, vehicle: Vehicle) -> Optional[ParkingSpot]:
# Best-fit: smallest spot that fits
candidates = [s for s in self.spots if s.is_free and s.can_fit(vehicle)]
return min(candidates, key=lambda s: s.size.value, default=None)
def park(self, vehicle: Vehicle) -> Optional[ParkingSpot]:
spot = self.find_free_spot(vehicle)
if spot:
spot.park(vehicle)
self._notify()
return spot
def vacate(self, spot: ParkingSpot):
spot.vacate()
self._notify()
def free_spots_by_size(self) -> dict[SpotSize, int]:
result = {s: 0 for s in SpotSize}
for spot in self.spots:
if spot.is_free:
result[spot.size] += 1
return result
class DisplayBoard(FloorObserver):
def update(self, floor: Floor):
counts = floor.free_spots_by_size()
print(f"[Floor {floor.floor_number}] Free — "
f"Small: {counts[SpotSize.SMALL]}, "
f"Medium: {counts[SpotSize.MEDIUM]}, "
f"Large: {counts[SpotSize.LARGE]}")
# --- Ticket ---
class Ticket:
def __init__(self, vehicle: Vehicle, spot: ParkingSpot, floor_number: int):
self.id = str(uuid.uuid4())[:8]
self.vehicle = vehicle
self.spot = spot
self.floor_number = floor_number
self.entry_time = datetime.now()
self.exit_time: Optional[datetime] = None
self.amount: Optional[float] = None
# --- Strategy pattern for pricing ---
class PricingStrategy(ABC):
@abstractmethod
def calculate(self, ticket: Ticket) -> float: pass
class HourlyPricing(PricingStrategy):
RATES = {
VehicleType.BIKE: 10,
VehicleType.CAR: 30,
VehicleType.TRUCK: 60,
}
def calculate(self, ticket: Ticket) -> float:
duration = ticket.exit_time - ticket.entry_time
hours = max(1, duration.total_seconds() / 3600) # min 1 hour
return self.RATES[ticket.vehicle.type] * hours
class FlatRatePricing(PricingStrategy):
RATES = {VehicleType.BIKE: 50, VehicleType.CAR: 150, VehicleType.TRUCK: 300}
def calculate(self, ticket: Ticket) -> float:
return self.RATES[ticket.vehicle.type]
# --- Gates ---
class EntryGate:
def __init__(self, lot):
self.lot = lot
def admit(self, vehicle: Vehicle) -> Optional[Ticket]:
return self.lot.park(vehicle)
class ExitGate:
def __init__(self, lot, pricing: PricingStrategy):
self.lot = lot
self.pricing = pricing
def process(self, ticket: Ticket) -> float:
ticket.exit_time = datetime.now()
ticket.amount = self.pricing.calculate(ticket)
self.lot.vacate(ticket)
return ticket.amount
# --- ParkingLot ---
class ParkingLot:
def __init__(self, floors: list[Floor]):
self.floors = floors
self.active_tickets: dict[str, Ticket] = {} # ticket_id -> Ticket
def park(self, vehicle: Vehicle) -> Optional[Ticket]:
for floor in self.floors:
spot = floor.park(vehicle)
if spot:
ticket = Ticket(vehicle, spot, floor.floor_number)
self.active_tickets[ticket.id] = ticket
return ticket
return None # lot full
def vacate(self, ticket: Ticket):
floor = self.floors[ticket.floor_number]
floor.vacate(ticket.spot)
self.active_tickets.pop(ticket.id, None)
# --- Usage ---
def build_lot():
floors = []
for f in range(3):
spots = (
[ParkingSpot(f"F{f}-S{i}", SpotSize.SMALL) for i in range(5)]
+ [ParkingSpot(f"F{f}-M{i}", SpotSize.MEDIUM) for i in range(10)]
+ [ParkingSpot(f"F{f}-L{i}", SpotSize.LARGE) for i in range(3)]
)
floor = Floor(f, spots)
floor.attach(DisplayBoard())
floors.append(floor)
return ParkingLot(floors)
lot = build_lot()
entry = EntryGate(lot)
exit_gate = ExitGate(lot, HourlyPricing())
t1 = entry.admit(Car("KA-01-1234"))
t2 = entry.admit(Bike("KA-01-5678"))
t3 = entry.admit(Truck("KA-01-9999"))
import time; time.sleep(0.1)
amount = exit_gate.process(t1)
print(f"\nCar paid: ₹{amount:.2f}")
Step 7 — Extensions & Tradeoffs
If the interviewer asks “What if we needed…”:
- Reserved spots (electric, handicapped, VIP): add a
reserved_forfield onParkingSpotand filter accordingly duringfind_free_spot. - Concurrency (multiple gates admitting at once): wrap spot acquisition in a lock per floor. Or use a concurrent reservation system.
- Different pricing per day/time: introduce a
TimeBasedPricingStrategythat wraps hourly rates with multipliers. - Membership-based discounts: introduce a
DiscountStrategyapplied on top ofPricingStrategy(Decorator pattern). - Lost ticket: define a special
LostTicketPricingstrategy (flat heavy fee). - Persistence: extract a
ParkingRepositoryinterface (DIP); back it with in-memory or DB. - Multiple entry/exit gates: gates are already independent; they all share the same
lotreference. With concurrency, we’d need lot-level locking.
Common Pitfalls in This Problem
- Putting pricing logic inside
ExitGateorTicket— violates SRP and OCP. Isolate it as a Strategy. - One
ParkingSpotclass handling all sizes via conditionals — works, but rigid. Using aSpotSizeenum +can_fit()is clean and extensible. - Making
ParkingLota Singleton — often unnecessary. If you want “one global lot,” pass it around via DI. - Ignoring display boards — interviewers often ask; always have a story for “how does the user see availability?”
- Forgetting the “full lot” case — your
park()must return something meaningful (None, or raise).
Patterns Used
- Strategy —
PricingStrategy(pluggable billing). - Observer —
DisplayBoardobservesFloorchanges. - Template Method (subtle) —
Vehiclesets up the skeleton; subclasses fill intypeandrequired_size.
SOLID Checks
- SRP ✓ — Vehicle knows vehicle stuff; Spot knows spot stuff; Pricing knows pricing; DisplayBoard knows display; Gate knows entry/exit flow.
- OCP ✓ — Add a new vehicle type, new spot size, new pricing strategy, new display type — all without modifying existing code.
- LSP ✓ — All
Vehiclesubclasses honor the same contract. - ISP ✓ — Interfaces (
PricingStrategy,FloorObserver) are narrow. - DIP ✓ —
ExitGatedepends onPricingStrategy(abstraction), not a concrete pricer.
5. Problem 2 — Design an Elevator System
The Prompt: Design a multi-elevator system for a building with N floors.
Elevators are trickier than parking lots because of state transitions and request scheduling. This is a great problem to showcase the State pattern.
Step 1 — Clarify Requirements
Questions to ask:
- How many elevators and floors? → M elevators, N floors (configurable).
- Two types of requests: external (press button on a floor, want to go up/down) and internal (inside elevator, pick destination). Support both? → Yes.
- Elevator directions? → Moves up, down, or idle.
- Door opens/closes? → Yes, as part of the state.
- Multiple elevators — how are requests assigned? → Need a scheduler/dispatcher.
- Scheduling algorithm? → Nearest-idle-elevator for this interview, but make it pluggable.
- Capacity limits? → Optional; mention as an extension.
- Emergency stop? → Out of scope for core design; mention as extension.
- Real-time / concurrency? → Simplify: one request at a time, single-threaded.
Scope summary: “I’ll design N floors, M elevators, each elevator having states (idle, moving up/down, doors opening/closing), external and internal request types, and a pluggable dispatcher that selects which elevator handles a request. Concurrency, capacity, and emergency modes are extensions.”
Step 2 — Identify Entities
Building— holds floors and elevators.Floor— represents a level; has up/down external buttons.Elevator— the car itself.ElevatorState— Idle, MovingUp, MovingDown, DoorOpen, DoorClosed.Request— abstract;ExternalRequest(from floor) andInternalRequest(from inside).Direction— enum (UP, DOWN, IDLE).Dispatcher— assigns requests to elevators.SchedulingStrategy— pluggable algorithm for selection.
Step 3 — Identify Actions
- User presses floor button (external request): dispatcher picks an elevator, routes the request.
- User inside elevator presses floor number (internal request): added to that elevator’s queue.
- Elevator moves between floors, opens doors, closes doors, continues or idles.
- Display shows current floor per elevator.
Step 4 — Relationships
Buildinghas-manyFloorsand has-manyElevators.Elevatorhas-oneElevatorState(changes over time).Elevatormaintains a set of target floors (pending stops).Dispatcheruses-aSchedulingStrategy(DIP).Request→ExternalRequestandInternalRequest(inheritance).
Step 5 — SOLID and Patterns
- State —
ElevatorStatehierarchy models the elevator’s lifecycle cleanly.Idle,MovingUp,MovingDown,DoorOpeneach encapsulate their behavior and transitions. - Strategy —
SchedulingStrategylets us swap algorithms (nearest, round-robin, least-busy). - Command (subtle) —
Requestobjects carry the action to perform. - Observer (optional) — floor displays could observe elevator position.
- DIP —
Dispatcherdepends onSchedulingStrategyabstraction. - OCP — new scheduling strategies or states can be added cleanly.
Step 6 — Code
from abc import ABC, abstractmethod
from enum import Enum
from typing import Optional
import heapq
class Direction(Enum):
UP = 1
DOWN = -1
IDLE = 0
# --- Request types ---
class Request(ABC):
@property
@abstractmethod
def floor(self) -> int: pass
class ExternalRequest(Request):
def __init__(self, source_floor: int, direction: Direction):
self._floor = source_floor
self.direction = direction
@property
def floor(self): return self._floor
def __repr__(self):
return f"ExternalRequest(floor={self._floor}, dir={self.direction.name})"
class InternalRequest(Request):
def __init__(self, destination: int):
self._floor = destination
@property
def floor(self): return self._floor
def __repr__(self):
return f"InternalRequest(dest={self._floor})"
# --- State pattern for elevator ---
class ElevatorState(ABC):
@abstractmethod
def step(self, elevator): pass
@abstractmethod
def name(self) -> str: pass
class Idle(ElevatorState):
def step(self, elevator):
if elevator.has_pending_requests():
target = elevator.next_target()
if target > elevator.current_floor:
elevator.set_state(MovingUp())
elif target < elevator.current_floor:
elevator.set_state(MovingDown())
else:
elevator.set_state(DoorOpen())
def name(self): return "IDLE"
class MovingUp(ElevatorState):
def step(self, elevator):
elevator.current_floor += 1
print(f"[Elevator {elevator.id}] Moving UP → floor {elevator.current_floor}")
if elevator.current_floor in elevator.stops:
elevator.set_state(DoorOpen())
def name(self): return "MOVING_UP"
class MovingDown(ElevatorState):
def step(self, elevator):
elevator.current_floor -= 1
print(f"[Elevator {elevator.id}] Moving DOWN → floor {elevator.current_floor}")
if elevator.current_floor in elevator.stops:
elevator.set_state(DoorOpen())
def name(self): return "MOVING_DOWN"
class DoorOpen(ElevatorState):
def step(self, elevator):
print(f"[Elevator {elevator.id}] Doors open at floor {elevator.current_floor}")
elevator.stops.discard(elevator.current_floor)
elevator.set_state(DoorClosed())
def name(self): return "DOOR_OPEN"
class DoorClosed(ElevatorState):
def step(self, elevator):
print(f"[Elevator {elevator.id}] Doors closed")
if elevator.has_pending_requests():
target = elevator.next_target()
if target > elevator.current_floor:
elevator.set_state(MovingUp())
elif target < elevator.current_floor:
elevator.set_state(MovingDown())
else:
elevator.set_state(DoorOpen())
else:
elevator.set_state(Idle())
def name(self): return "DOOR_CLOSED"
# --- Elevator ---
class Elevator:
def __init__(self, id: int, num_floors: int):
self.id = id
self.num_floors = num_floors
self.current_floor = 0
self.stops: set[int] = set()
self._state: ElevatorState = Idle()
def set_state(self, state: ElevatorState):
self._state = state
@property
def state_name(self): return self._state.name()
def step(self):
self._state.step(self)
def accept(self, request: Request):
self.stops.add(request.floor)
if isinstance(self._state, Idle):
self._state.step(self) # Wake it up
def has_pending_requests(self):
return bool(self.stops)
def next_target(self) -> int:
# Simple: go to nearest stop
return min(self.stops, key=lambda f: abs(f - self.current_floor))
@property
def direction(self) -> Direction:
if isinstance(self._state, MovingUp): return Direction.UP
if isinstance(self._state, MovingDown): return Direction.DOWN
return Direction.IDLE
# --- Strategy for dispatching ---
class SchedulingStrategy(ABC):
@abstractmethod
def select(self, elevators: list[Elevator], request: Request) -> Elevator: pass
class NearestCarStrategy(SchedulingStrategy):
def select(self, elevators, request):
# Idle elevators are best; else pick the one whose current trajectory matches
def score(e: Elevator):
distance = abs(e.current_floor - request.floor)
# Prefer idle (bonus of 0); penalize if direction disagrees
if e.direction == Direction.IDLE:
return distance
# Same direction as request is better
moving_toward = (
(e.direction == Direction.UP and request.floor >= e.current_floor) or
(e.direction == Direction.DOWN and request.floor <= e.current_floor)
)
return distance + (0 if moving_toward else 100)
return min(elevators, key=score)
class RoundRobinStrategy(SchedulingStrategy):
def __init__(self):
self._index = 0
def select(self, elevators, request):
chosen = elevators[self._index % len(elevators)]
self._index += 1
return chosen
# --- Dispatcher ---
class Dispatcher:
def __init__(self, elevators: list[Elevator], strategy: SchedulingStrategy):
self.elevators = elevators
self.strategy = strategy
def submit(self, request: Request, for_elevator: Optional[Elevator] = None):
if isinstance(request, InternalRequest) and for_elevator:
for_elevator.accept(request)
else:
chosen = self.strategy.select(self.elevators, request)
print(f">>> Routing {request} to Elevator {chosen.id}")
chosen.accept(request)
# --- Simulation tick ---
class Building:
def __init__(self, num_floors: int, num_elevators: int,
strategy: SchedulingStrategy):
self.num_floors = num_floors
self.elevators = [Elevator(i, num_floors) for i in range(num_elevators)]
self.dispatcher = Dispatcher(self.elevators, strategy)
def tick(self):
"""One time unit — every elevator advances its state."""
for e in self.elevators:
e.step()
# --- Usage ---
building = Building(num_floors=10, num_elevators=2, strategy=NearestCarStrategy())
# Simulate some requests
building.dispatcher.submit(ExternalRequest(5, Direction.UP))
for _ in range(12):
building.tick()
# After elevator reaches floor 5, rider presses "floor 8"
if building.elevators[0].current_floor == 5 and 8 not in building.elevators[0].stops:
building.dispatcher.submit(InternalRequest(8), for_elevator=building.elevators[0])
Step 7 — Extensions & Tradeoffs
- Scheduling algorithm sophistication — real elevator algorithms (SCAN, LOOK, SSTF) can replace
NearestCarStrategyas new strategies. OCP wins. - Capacity limit — add
max_weightormax_persons; reject internal requests that would overflow. - Concurrency — in reality, requests come from multiple floors concurrently. Wrap dispatcher state with a lock, or use a queue processed by a single worker.
- Emergency stop / maintenance mode — add
EmergencyStopped,UnderMaintenancestates. - Floor displays —
Floorcould observe each elevator’s position via the Observer pattern. - Predictive dispatching — assign elevators based on historical traffic patterns (ML-heavy; out of interview scope but worth mentioning).
Common Pitfalls
- Representing states as strings/enums — works but pushes all behavior into
if/elif. The State pattern is cleaner here and is exactly the kind of thing interviewers love to see. - Coupling dispatcher to nearest-car logic — hard-wiring the algorithm. Use Strategy so alternatives swap in.
- One massive
Elevator.move()method with nested conditionals — fragile and untestable. State objects fix this. - Forgetting external vs internal requests have different routing rules — internal requests go to the specific elevator; external go through the dispatcher.
Patterns Used
- State — elevator states (Idle, MovingUp, MovingDown, DoorOpen, DoorClosed).
- Strategy —
SchedulingStrategyfor dispatching. - Command (subtle) —
Requestobjects encapsulate actions.
SOLID Checks
- SRP ✓ — State transitions in state classes, dispatching in dispatcher, scheduling in strategy.
- OCP ✓ — Add a new State (emergency), new Strategy (SCAN algorithm) — no existing code changes.
- LSP ✓ — All states substitute for
ElevatorStateproperly. - ISP ✓ — Interfaces are narrow (
SchedulingStrategy.select,ElevatorState.step). - DIP ✓ — Dispatcher depends on abstract
SchedulingStrategy.
6. Problem 3 — Design a Library Management System
The Prompt: Design a library system that supports members borrowing books, returning, and reserving.
Library systems showcase relationships (members ↔ books), lifecycle (book states), and policy (borrowing rules).
Step 1 — Clarify Requirements
Questions to ask:
- Book copies? → Yes; multiple physical copies per title.
- Member types? → Student, Faculty, Guest — different borrowing limits and durations.
- Reservations? → A member can reserve a currently-borrowed book; first in queue gets it when returned.
- Fines for late returns? → Yes; per day after due date.
- Search capabilities? → By title, author, category — basic in-memory search.
- Multiple branches/libraries? → Single library for core design; extension for multi-branch.
- Concurrency? → Single-threaded for simplicity.
- Payment? → Out of scope; just track fine amounts.
Scope summary: “I’ll design a library with book titles and copies, member types with different policies, a borrowing flow with due dates and fines, a reservation queue, and search. Multi-branch, payment processing, and concurrency are extensions.”
Step 2 — Identify Entities
Library— the system root.BookItem(a physical copy) vsBook(the title).Member— abstract;Student,Faculty,Guestsubclasses.Loan— represents a borrowing record.Reservation— a waiting request for a book.MembershipPolicy— limits + durations + fine rates per member type.Librarian— actor who performs administrative actions.Search— a component for querying books.
Key distinction: Book is the abstract title (1984 by Orwell); BookItem is a specific copy (ISBN + physical copy ID). Many interviewers look for this distinction.
Step 3 — Identify Actions
- Member borrows a book copy → creates a Loan.
- Member returns a book → closes the Loan, potentially assesses fine, triggers next reservation.
- Member reserves a book → adds to reservation queue.
- Librarian adds/removes books.
- System searches by title/author/category.
Step 4 — Relationships
Bookhas-manyBookItem(copies).BookItemhas a status: AVAILABLE, BORROWED, RESERVED.Memberhas-manyLoans.Memberis linked to aMembershipPolicy(Strategy-like).Reservationlinks aMemberto aBook.Libraryhas-manyBooks,Members,Loans,Reservations.
Step 5 — SOLID and Patterns
- Strategy —
MembershipPolicy(varies per member type: borrowing limits, loan durations, fine rates). - Observer (optional) — reservations notify members when their book becomes available.
- State (optional) —
BookItem.statuscould use State pattern, but since logic is simple, an enum + checks is adequate. This is a judgment call — interviewers appreciate hearing “I could use State here but it’s overkill for this flow.” - Factory —
MemberFactorycould produce the right member type + policy. - SRP — policy logic separated from member; search separated from library core.
Step 6 — Code
from abc import ABC, abstractmethod
from enum import Enum
from datetime import datetime, timedelta
from typing import Optional
from collections import deque
import uuid
# --- Enums ---
class BookStatus(Enum):
AVAILABLE = "available"
BORROWED = "borrowed"
LOST = "lost"
# --- Domain entities ---
class Book:
"""The title (abstract book)."""
def __init__(self, isbn: str, title: str, author: str, category: str):
self.isbn = isbn
self.title = title
self.author = author
self.category = category
self.copies: list[BookItem] = []
def add_copy(self, copy: "BookItem"):
self.copies.append(copy)
def available_copy(self) -> Optional["BookItem"]:
return next((c for c in self.copies if c.status == BookStatus.AVAILABLE), None)
class BookItem:
"""A physical copy of a book."""
def __init__(self, book: Book, copy_id: str):
self.book = book
self.copy_id = copy_id
self.status = BookStatus.AVAILABLE
# --- Strategy for membership policy ---
class MembershipPolicy(ABC):
@property
@abstractmethod
def max_books(self) -> int: pass
@property
@abstractmethod
def loan_days(self) -> int: pass
@property
@abstractmethod
def fine_per_day(self) -> float: pass
class StudentPolicy(MembershipPolicy):
@property
def max_books(self): return 3
@property
def loan_days(self): return 14
@property
def fine_per_day(self): return 5.0
class FacultyPolicy(MembershipPolicy):
@property
def max_books(self): return 10
@property
def loan_days(self): return 30
@property
def fine_per_day(self): return 2.0
class GuestPolicy(MembershipPolicy):
@property
def max_books(self): return 1
@property
def loan_days(self): return 7
@property
def fine_per_day(self): return 10.0
# --- Member ---
class Member:
def __init__(self, member_id: str, name: str, policy: MembershipPolicy):
self.member_id = member_id
self.name = name
self.policy = policy
self.active_loans: list[Loan] = []
def can_borrow_more(self) -> bool:
return len(self.active_loans) < self.policy.max_books
# --- Loan ---
class Loan:
def __init__(self, member: Member, book_item: BookItem):
self.id = str(uuid.uuid4())[:8]
self.member = member
self.book_item = book_item
self.issue_date = datetime.now()
self.due_date = self.issue_date + timedelta(days=member.policy.loan_days)
self.return_date: Optional[datetime] = None
@property
def is_overdue(self) -> bool:
return datetime.now() > self.due_date and self.return_date is None
def calculate_fine(self) -> float:
end_date = self.return_date or datetime.now()
if end_date <= self.due_date:
return 0.0
overdue_days = (end_date - self.due_date).days + 1
return overdue_days * self.member.policy.fine_per_day
# --- Reservation ---
class Reservation:
def __init__(self, member: Member, book: Book):
self.member = member
self.book = book
self.reserved_on = datetime.now()
# --- Search ---
class BookSearch:
def __init__(self, books: list[Book]):
self.books = books
def by_title(self, title: str) -> list[Book]:
return [b for b in self.books if title.lower() in b.title.lower()]
def by_author(self, author: str) -> list[Book]:
return [b for b in self.books if author.lower() in b.author.lower()]
def by_category(self, category: str) -> list[Book]:
return [b for b in self.books if b.category.lower() == category.lower()]
# --- Library (the facade/coordinator) ---
class Library:
def __init__(self):
self.books: dict[str, Book] = {} # isbn -> Book
self.members: dict[str, Member] = {}
self.active_loans: dict[str, Loan] = {} # loan_id -> Loan
self.reservations: dict[str, deque[Reservation]] = {} # isbn -> queue
# Book management
def add_book(self, book: Book):
self.books[book.isbn] = book
def add_member(self, member: Member):
self.members[member.member_id] = member
@property
def search(self) -> BookSearch:
return BookSearch(list(self.books.values()))
# Core flow
def borrow(self, member_id: str, isbn: str) -> Optional[Loan]:
member = self.members[member_id]
book = self.books[isbn]
if not member.can_borrow_more():
print(f"{member.name} has reached borrowing limit.")
return None
copy = book.available_copy()
if not copy:
print(f"No copies available for '{book.title}'. Consider reserving.")
return None
copy.status = BookStatus.BORROWED
loan = Loan(member, copy)
member.active_loans.append(loan)
self.active_loans[loan.id] = loan
print(f"Issued '{book.title}' to {member.name}. Due: {loan.due_date.date()}")
return loan
def return_book(self, loan_id: str) -> float:
loan = self.active_loans.pop(loan_id)
loan.return_date = datetime.now()
fine = loan.calculate_fine()
loan.book_item.status = BookStatus.AVAILABLE
loan.member.active_loans.remove(loan)
print(f"Returned '{loan.book_item.book.title}'. Fine: ₹{fine:.2f}")
# Check reservation queue
isbn = loan.book_item.book.isbn
queue = self.reservations.get(isbn)
if queue:
next_reservation = queue.popleft()
print(f" Notifying {next_reservation.member.name}: "
f"'{loan.book_item.book.title}' is now available for you.")
return fine
def reserve(self, member_id: str, isbn: str):
member = self.members[member_id]
book = self.books[isbn]
if book.available_copy():
print(f"Book '{book.title}' is available — just borrow it.")
return
self.reservations.setdefault(isbn, deque()).append(Reservation(member, book))
print(f"{member.name} reserved '{book.title}'.")
# --- Usage ---
library = Library()
# Add books
b1 = Book("978-001", "The Pragmatic Programmer", "Hunt & Thomas", "Tech")
b1.add_copy(BookItem(b1, "c1"))
b1.add_copy(BookItem(b1, "c2"))
library.add_book(b1)
# Add members
amit = Member("M1", "Amit", StudentPolicy())
priya = Member("M2", "Priya", FacultyPolicy())
library.add_member(amit)
library.add_member(priya)
# Flow
loan1 = library.borrow("M1", "978-001")
loan2 = library.borrow("M2", "978-001")
loan3 = library.borrow("M1", "978-001") # No copies — should reserve
library.reserve("M1", "978-001")
library.return_book(loan1.id) # Should notify Amit
Step 7 — Extensions & Tradeoffs
- Multiple branches — introduce a
Branchclass;Librarybecomes a network of branches. Books can be transferred between branches. - Digital books / e-books — abstract
BorrowableItem;BookItemandEbookLicensebecome subtypes. - Notifications — Observer pattern: when a reservation becomes available, notify the member via email/SMS.
- Fine payment flow — add a
Paymentcomponent; extendreturn_bookto accept payment. - Membership upgrade — Policy is already a strategy — just swap the
policyattribute. - Popularity tracking — add a
BorrowingHistoryper book; use for recommendations (out of scope but a great mention).
Common Pitfalls
- Not distinguishing
Book(title) fromBookItem(copy) — very common mistake; leads to modeling headaches when the library has 10 copies of the same book. - Stuffing all policy logic inside
Member— violates SRP and makes it hard to add new member types. - Ignoring the reservation queue — interviewers ask about this often.
- Fines computed in
Library— should be onLoan(it has the data) or a dedicatedFineCalculatorif logic grows complex.
Patterns Used
- Strategy —
MembershipPolicy(student/faculty/guest). - Facade —
Libraryprovides a simple interface over the internal moving parts. - Repository-like pattern (implicitly) —
self.books,self.membersare in-memory repositories.
SOLID Checks
- SRP ✓ — Policies, loans, searches, reservations, and library administration are separate.
- OCP ✓ — New member types (Premium, Alumni) = new policy classes, no modification.
- LSP ✓ — All
MembershipPolicyvariants are substitutable. - ISP ✓ —
MembershipPolicyhas a focused interface. - DIP ✓ —
Memberdepends onMembershipPolicyabstraction, not a concrete policy.
7. Problem 4 — Design a Vending Machine
The Prompt: Design a vending machine that sells items, accepts coins/notes, returns change, and handles inventory.
Vending machines are the canonical State pattern interview problem. A clean design here immediately shouts “this candidate knows the State pattern.”
Step 1 — Clarify Requirements
Questions to ask:
- Items have a fixed set of prices and quantities? → Yes; inventory is pre-loaded.
- Accepted denominations? → Indian coins + notes: ₹1, ₹2, ₹5, ₹10, ₹20, ₹50, ₹100.
- Exact change required? → No; machine gives change if possible. If not enough change, reject or notify.
- Cancel button (refund without purchase)? → Yes.
- Restocking? → Yes; administrator interface.
- Concurrency? → Assume one user at a time.
Scope summary: “I’ll design a vending machine with product inventory, multi-denomination payment, a clear state lifecycle (idle → has money → dispensing), change-giving, cancel/refund, and restock. Concurrency and payment gateways are extensions.”
Step 2 — Identify Entities
VendingMachine— the overall system.Product— an item sold (name, price, code).Inventory— tracks product stock and coin/note availability.VendingMachineState— Idle, HasMoney, Dispensing, OutOfStock (State pattern).Coin/Note— denominations.PaymentProcessor— handles coin/note insertion and change calculation.Administrator— restocks and collects money.
Step 3 — Identify Actions
- Insert coin/note → adds to current balance; transitions Idle → HasMoney.
- Select product → checks stock and balance; dispenses + returns change; transitions HasMoney → Dispensing → Idle.
- Cancel → refund the balance; transitions back to Idle.
- Restock (admin) → add items / denominations.
Step 4 — Relationships
VendingMachinehas-aInventory, has-a currentVendingMachineState, has-a currentbalance.Stateclasses know how to transition the machine.Inventoryhas-many product slots + denomination counts.
Step 5 — SOLID and Patterns
- State — the heart of the design. Behavior changes drastically based on current state.
- Strategy (optional) —
ChangeStrategyfor computing change (greedy vs exact-only). - Command (optional) — represent actions as commands for logging/undo. Often overkill for this problem.
- SRP — inventory management separate from state management, payment separate from dispensing.
Step 6 — Code
from abc import ABC, abstractmethod
from enum import Enum
from typing import Optional
# --- Product ---
class Product:
def __init__(self, code: str, name: str, price: int):
self.code = code
self.name = name
self.price = price # in rupees (integer for simplicity)
# --- Inventory ---
class Inventory:
def __init__(self):
self.products: dict[str, tuple[Product, int]] = {} # code -> (product, qty)
self.coins: dict[int, int] = {1: 0, 2: 0, 5: 0, 10: 0, 20: 0, 50: 0, 100: 0}
def add_product(self, product: Product, qty: int):
if product.code in self.products:
p, existing = self.products[product.code]
self.products[product.code] = (p, existing + qty)
else:
self.products[product.code] = (product, qty)
def stock_coins(self, denomination: int, count: int):
if denomination not in self.coins:
raise ValueError(f"Invalid denomination: {denomination}")
self.coins[denomination] += count
def get_product(self, code: str) -> Optional[Product]:
if code not in self.products: return None
return self.products[code][0]
def has_stock(self, code: str) -> bool:
return code in self.products and self.products[code][1] > 0
def remove_one(self, code: str):
p, qty = self.products[code]
if qty == 0:
raise ValueError("Out of stock")
self.products[code] = (p, qty - 1)
def can_provide_change(self, amount: int) -> bool:
# Greedy check — can we make this amount?
remaining = amount
for denom in sorted(self.coins.keys(), reverse=True):
use = min(self.coins[denom], remaining // denom)
remaining -= use * denom
return remaining == 0
def dispense_change(self, amount: int) -> dict[int, int]:
dispensed = {}
remaining = amount
for denom in sorted(self.coins.keys(), reverse=True):
use = min(self.coins[denom], remaining // denom)
if use > 0:
dispensed[denom] = use
self.coins[denom] -= use
remaining -= use * denom
if remaining != 0:
raise ValueError("Can't make exact change")
return dispensed
# --- State pattern ---
class VendingState(ABC):
@abstractmethod
def insert_money(self, machine, denomination: int): pass
@abstractmethod
def select_product(self, machine, code: str): pass
@abstractmethod
def cancel(self, machine): pass
class Idle(VendingState):
def insert_money(self, machine, denomination):
machine.inventory.stock_coins(denomination, 1) # machine absorbs it
machine.balance += denomination
print(f"[Idle → HasMoney] Balance: ₹{machine.balance}")
machine.set_state(HasMoney())
def select_product(self, machine, code):
print("Insert money first.")
def cancel(self, machine):
print("Nothing to cancel.")
class HasMoney(VendingState):
def insert_money(self, machine, denomination):
machine.inventory.stock_coins(denomination, 1)
machine.balance += denomination
print(f"Balance: ₹{machine.balance}")
def select_product(self, machine, code):
product = machine.inventory.get_product(code)
if not product:
print(f"Unknown product code: {code}"); return
if not machine.inventory.has_stock(code):
print(f"'{product.name}' is out of stock"); return
if machine.balance < product.price:
print(f"Insufficient balance. Need ₹{product.price - machine.balance} more."); return
change = machine.balance - product.price
if change > 0 and not machine.inventory.can_provide_change(change):
print(f"Can't give ₹{change} change. Please use exact amount or cancel.")
return
print(f"[HasMoney → Dispensing]")
machine.set_state(Dispensing())
machine.dispense(product, change)
def cancel(self, machine):
# Refund — but we've already pooled the coins. In reality we'd track separately.
refund = machine.inventory.dispense_change(machine.balance)
print(f"Refunding ₹{machine.balance} → {refund}")
machine.balance = 0
machine.set_state(Idle())
class Dispensing(VendingState):
def insert_money(self, m, d):
print("Busy dispensing. Please wait.")
def select_product(self, m, code):
print("Busy dispensing.")
def cancel(self, m):
print("Can't cancel during dispense.")
# --- Vending machine ---
class VendingMachine:
def __init__(self, inventory: Inventory):
self.inventory = inventory
self.balance = 0
self._state: VendingState = Idle()
def set_state(self, state: VendingState):
self._state = state
def insert_money(self, denomination: int):
self._state.insert_money(self, denomination)
def select_product(self, code: str):
self._state.select_product(self, code)
def cancel(self):
self._state.cancel(self)
def dispense(self, product: Product, change: int):
self.inventory.remove_one(product.code)
print(f"Dispensed: {product.name}")
if change > 0:
change_dispensed = self.inventory.dispense_change(change)
print(f"Change: ₹{change} → {change_dispensed}")
self.balance = 0
self.set_state(Idle())
# --- Usage ---
inv = Inventory()
inv.add_product(Product("A1", "Coke", 30), 5)
inv.add_product(Product("A2", "Chips", 20), 5)
inv.add_product(Product("A3", "Water", 15), 5)
for denom in [1, 2, 5, 10, 20]:
inv.stock_coins(denom, 20) # pre-stocked change
vm = VendingMachine(inv)
vm.select_product("A1") # Error: no money
vm.insert_money(20)
vm.insert_money(10)
vm.insert_money(5) # Balance ₹35
vm.select_product("A1") # Dispense Coke, ₹5 change
Step 7 — Extensions & Tradeoffs
- Exact-change-only mode — when low on change denominations, machine refuses overpayments. Add a
ChangeStrategy. - Multiple products in one transaction — change the state machine to allow cart-like behavior. Significantly more complex — mention but don’t over-engineer.
- Digital payment — add
PaymentMethodabstraction;CashPayment,UPIPayment,CardPaymentas subtypes. - Hardware failure handling — states like
JammedorOutOfService. - Admin portal — restocking, coin retrieval, sales reports — probably its own class.
- Concurrency — locking around state transitions (only one user at a time anyway; often not needed).
Common Pitfalls
- Not using State pattern — easy to fall into one big class with
if self.state == "idle"everywhere. Interviewers often explicitly hint: “think about how the machine’s behavior differs based on its current state.” - Confusing balance (inserted money) with inventory coins (available for change) — in reality these are separate pools; simplifying them into one (as I did) is a modeling choice worth noting.
- Forgetting the “no change available” case — a real vending-machine pain; candidates miss this frequently.
- Treating product code as name — keep them distinct (code for selection, name for display).
Patterns Used
- State — core of the design; Idle, HasMoney, Dispensing.
- Strategy (implicitly in change logic; explicit if extended).
- Facade —
VendingMachinefronts a subsystem (inventory + state + dispenser).
SOLID Checks
- SRP ✓ — Inventory, State classes, and VendingMachine each have focused roles.
- OCP ✓ — New states (jammed, maintenance), new denominations, new products — all additive.
- LSP ✓ — All states substitute uniformly.
- ISP ✓ — State interface is narrow (3 methods).
- DIP ✓ — VendingMachine holds a state interface, not a concrete state.
8. Problem 5 — Design a Splitwise-Like Expense Sharing System
The Prompt: Design a system where users can record shared expenses (e.g., “I paid ₹1200 for dinner; split equally among 4 people”) and track who owes whom.
This problem stretches beyond simple entity modeling — it exercises policy (split strategies), graph-like debt tracking, and observability of complex state.
Step 1 — Clarify Requirements
Questions to ask:
- How can expenses be split? → Equally, by exact amounts, by percentage, or by shares.
- Currency handling? → Single currency for simplicity.
- Groups? → Yes; users can be in multiple groups; expenses are typically group-scoped but can be one-off.
- Debt simplification? → Should the system suggest the minimum number of settlements to clear debts? → Yes — a nice add-on showcasing algorithms.
- Payments/settlements? → Recording a “I paid X to Y” action reduces debt.
- Notifications? → Out of scope; mention Observer pattern as extension.
Scope summary: “Users, groups, expenses split by various strategies, a balance ledger showing who owes whom, settlement recording, and optional debt simplification. Notifications and multi-currency are extensions.”
Step 2 — Identify Entities
User— a person in the system.Group— a set of users sharing expenses.Expense— a record of who paid how much and who shares in what way.SplitStrategy— equal, exact, percentage, shares.Split— an individual user’s portion of an expense.BalanceSheet— tracks pairwise balances (who owes whom).ExpenseManager— the facade coordinating it all.
Step 3 — Identify Actions
- Create user, group.
- Add expense (paid by X, split among Y with strategy Z).
- Record settlement (user A pays user B).
- View balance (for a user, or for a group).
- Simplify debts in a group.
Step 4 — Relationships
Grouphas-manyUsers.Expensehas-one payer (User), has-manySplits(each tied to a user).BalanceSheetis a map{(user_a, user_b): amount}where positive means a owes b.ExpenseManagerorchestrates.
Step 5 — SOLID and Patterns
- Strategy —
SplitStrategyis the cleanest fit: different algorithms for splitting. - Facade —
ExpenseManagerprovides a simple API over users/groups/expenses/balances. - Command (optional) — each action (add expense, settle) could be a Command for undo/audit. Usually out of scope.
- Observer (optional) — balance updates could notify UI.
- SRP — each concern isolated.
Step 6 — Code
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Optional
import uuid
# --- User / Group ---
@dataclass
class User:
user_id: str
name: str
email: str
def __hash__(self): return hash(self.user_id)
def __eq__(self, other): return isinstance(other, User) and self.user_id == other.user_id
@dataclass
class Group:
group_id: str
name: str
members: list[User] = field(default_factory=list)
def add_member(self, user: User):
if user not in self.members:
self.members.append(user)
# --- Split ---
@dataclass
class Split:
user: User
amount: float
# --- Strategy for splitting ---
class SplitStrategy(ABC):
@abstractmethod
def calculate(self, total: float, participants: list[User], **kwargs) -> list[Split]: pass
class EqualSplit(SplitStrategy):
def calculate(self, total, participants, **kwargs):
n = len(participants)
share = round(total / n, 2)
splits = [Split(u, share) for u in participants]
# Adjust last split for rounding drift
drift = total - share * n
splits[-1] = Split(splits[-1].user, round(splits[-1].amount + drift, 2))
return splits
class ExactSplit(SplitStrategy):
def calculate(self, total, participants, *, exact_amounts: dict[User, float], **kwargs):
if abs(sum(exact_amounts.values()) - total) > 0.01:
raise ValueError("Exact amounts don't sum to total")
return [Split(u, exact_amounts[u]) for u in participants]
class PercentSplit(SplitStrategy):
def calculate(self, total, participants, *, percentages: dict[User, float], **kwargs):
if abs(sum(percentages.values()) - 100) > 0.01:
raise ValueError("Percentages don't sum to 100")
return [Split(u, round(total * percentages[u] / 100, 2)) for u in participants]
# --- Expense ---
@dataclass
class Expense:
id: str
description: str
amount: float
payer: User
splits: list[Split]
group: Optional[Group] = None
# --- Balance sheet ---
class BalanceSheet:
"""
Stores pairwise debts. balances[a][b] = how much a owes b.
Invariant: balances[a][b] and balances[b][a] are not both nonzero;
we net them out.
"""
def __init__(self):
self._balances: dict[User, dict[User, float]] = {}
def _get(self, a: User, b: User) -> float:
return self._balances.get(a, {}).get(b, 0.0)
def _set(self, a: User, b: User, amount: float):
self._balances.setdefault(a, {})[b] = amount
def add_debt(self, debtor: User, creditor: User, amount: float):
"""debtor owes creditor `amount` more."""
if debtor == creditor: return
# Net against reverse direction
reverse = self._get(creditor, debtor)
if reverse >= amount:
self._set(creditor, debtor, round(reverse - amount, 2))
else:
# Clear reverse, add remainder to forward
remainder = amount - reverse
self._set(creditor, debtor, 0.0)
self._set(debtor, creditor, round(self._get(debtor, creditor) + remainder, 2))
def get_balance(self, user: User) -> dict[User, float]:
"""Returns dict of other_user -> amount (positive = user owes them, negative = they owe user)."""
result = {}
for other, amt in self._balances.get(user, {}).items():
if amt > 0:
result[other] = amt
for other, inner in self._balances.items():
if other == user: continue
amt = inner.get(user, 0.0)
if amt > 0:
result[other] = result.get(other, 0) - amt
return result
def all_pairwise(self) -> list[tuple[User, User, float]]:
result = []
for debtor, inner in self._balances.items():
for creditor, amt in inner.items():
if amt > 0:
result.append((debtor, creditor, amt))
return result
# --- ExpenseManager (Facade) ---
class ExpenseManager:
def __init__(self):
self.users: dict[str, User] = {}
self.groups: dict[str, Group] = {}
self.expenses: list[Expense] = []
self.balance_sheet = BalanceSheet()
def add_user(self, user: User):
self.users[user.user_id] = user
def add_group(self, group: Group):
self.groups[group.group_id] = group
def add_expense(self, description: str, amount: float, payer: User,
participants: list[User], strategy: SplitStrategy,
group: Optional[Group] = None, **strategy_kwargs) -> Expense:
splits = strategy.calculate(amount, participants, **strategy_kwargs)
expense = Expense(
id=str(uuid.uuid4())[:8],
description=description, amount=amount,
payer=payer, splits=splits, group=group,
)
self.expenses.append(expense)
# Update balances: each non-payer participant owes the payer their split
for s in splits:
if s.user != payer:
self.balance_sheet.add_debt(s.user, payer, s.amount)
return expense
def record_settlement(self, from_user: User, to_user: User, amount: float):
"""A direct payment reduces debt from_user → to_user."""
self.balance_sheet.add_debt(to_user, from_user, amount)
print(f"Settled: {from_user.name} paid {to_user.name} ₹{amount}")
def show_balances(self, user: User):
balances = self.balance_sheet.get_balance(user)
if not balances:
print(f"{user.name}: all settled!")
return
for other, amt in balances.items():
if amt > 0:
print(f"{user.name} owes {other.name}: ₹{amt:.2f}")
else:
print(f"{other.name} owes {user.name}: ₹{-amt:.2f}")
# --- Usage ---
mgr = ExpenseManager()
amit = User("u1", "Amit", "amit@x.com")
priya = User("u2", "Priya", "priya@x.com")
rohan = User("u3", "Rohan", "rohan@x.com")
for u in [amit, priya, rohan]:
mgr.add_user(u)
trip = Group("g1", "Goa Trip")
for u in [amit, priya, rohan]:
trip.add_member(u)
mgr.add_group(trip)
# Amit pays ₹1200 for dinner; split equally among all 3
mgr.add_expense("Dinner", 1200, amit, [amit, priya, rohan],
EqualSplit(), group=trip)
# Priya pays ₹900 for cab; Amit owes 500, Rohan owes 400 (exact)
mgr.add_expense("Cab", 900, priya, [amit, priya, rohan],
ExactSplit(), group=trip,
exact_amounts={amit: 500, priya: 0, rohan: 400})
mgr.show_balances(amit)
mgr.show_balances(priya)
mgr.show_balances(rohan)
# Settlement
mgr.record_settlement(amit, priya, 100)
print("---")
mgr.show_balances(amit)
mgr.show_balances(priya)
Step 7 — Extensions & Tradeoffs
- Debt simplification — given current pairwise balances, find the minimum-transaction way to settle all. This is a graph problem (min-cashflow); use a greedy algorithm or netting approach. This is a common follow-up to this problem in interviews.
- Multiple currencies — store expenses in native currency; convert via a
CurrencyConverterwhen computing balances in a user’s preferred currency. - Multi-payer expenses — someone pays part, another pays part. Extend the
payerfield topayers: list[Split]. - Activity feed — Observer pattern: UI observes balance updates.
- Immutable history — represent each expense as a Command for auditability and potential undo.
- Concurrency — if multiple users add expenses simultaneously to a group, use proper transactions. Interview-level: mention and move on.
Common Pitfalls
- Conflating Expense and Split — Expense is the event; Split is one person’s share. Keep them separate.
- Not using Strategy for splitting — hardcoding equal/exact/percent with conditionals loses OCP.
- Double-counting balances — forgetting to subtract/net against reverse direction.
- Floating-point for money — interviewers often ask; in production use
Decimalor cents-as-int. Mention this tradeoff even if you use floats for demo. - Over-engineering the balance sheet — a simple dict-of-dicts works for interviews; distributed ledger stuff is out of scope.
Patterns Used
- Strategy —
SplitStrategyvariants. - Facade —
ExpenseManagerprovides a simple API over everything. - Composite (subtle) — Groups contain users; expenses compose splits.
SOLID Checks
- SRP ✓ — Split strategies, balance sheet, manager all separate.
- OCP ✓ — New split algorithm (e.g.,
SharesSplit) = new class. - LSP ✓ — All strategies substitutable.
- ISP ✓ —
SplitStrategyis narrow. - DIP ✓ — Manager depends on
SplitStrategyabstraction.
9. Problem 6 — Design a Ride-Hailing System
The Prompt: Design a ride-hailing service (like Uber/Ola) at the LLD level — riders request rides, drivers accept, trips complete, fares are computed.
This is the most open-ended of the six. It’s a great showcase for multiple patterns working together and for thinking about extensibility and scale.
Step 1 — Clarify Requirements
Questions to ask:
- Core flow? → Rider requests ride → system matches nearby driver → driver accepts → trip happens → fare calculated → payment recorded.
- Multiple ride types? → Yes: mini, sedan, SUV; each with different pricing.
- Surge pricing? → Yes, during high demand.
- Matching algorithm? → Nearest available driver to pickup; pluggable.
- Payment methods? → Card, UPI, wallet, cash; external gateway is out of scope.
- Ratings? → Both rider and driver rate each other.
- Concurrency? → Real system is distributed; for LLD, assume in-memory, optional mention of locking.
- Cancellation? → Yes; with potential fee depending on when canceled.
Scope summary: “I’ll design entities (User, Rider, Driver, Vehicle, Trip), a matching flow with pluggable strategies, a lifecycle state for trips, pricing with vehicle type and surge, rating capture, and payment recording. Real-time geolocation, persistence, and concurrency are extensions.”
Step 2 — Identify Entities
User(base),Rider,Driver— inheritance.Vehicle— driver owns one; attributes (type, plate).Trip— the core unit: rider, driver, vehicle, source, destination, fare, state.Location— (lat, lng) + helpers.TripState— Requested, DriverAssigned, Started, Completed, Cancelled (State pattern).MatchingStrategy— picks a driver for a ride request.PricingStrategy— computes fare.SurgeCalculator— multiplier for peak hours.PaymentMethod— abstract; subtypes for each method.RideManager— the orchestrator/facade.
Step 3 — Identify Actions
- Rider requests a ride.
- System matches a driver; driver accepts (or declines, falling through to next).
- Driver starts the trip upon pickup.
- Driver ends the trip at drop-off; fare is calculated.
- Payment is processed.
- Rider/driver rate each other.
- Cancellation can happen before driver accepts, before trip starts, or mid-trip — different fee rules.
Step 4 — Relationships
Driverhas-oneVehicle.Triphas-oneRider, has-oneDriver, has-oneVehicle, has-oneTripState.RideManagerhas-many drivers, has-many trips.MatchingStrategy,PricingStrategy,SurgeCalculatorare dependencies ofRideManager.
Step 5 — SOLID and Patterns
- State —
TripState(Requested, DriverAssigned, Started, Completed, Cancelled). - Strategy — matching algorithm, pricing algorithm, payment method — three strategies! This problem naturally attracts multiple Strategy applications.
- Observer (optional) — drivers observe ride-request events in their area; riders observe trip state changes for the app UI.
- Factory (optional) —
PaymentMethodFactoryfor creating payment objects. - Facade —
RideManageras the orchestrator. - SRP — each component owns one concern.
- DIP — manager depends on strategy abstractions.
Step 6 — Code
from abc import ABC, abstractmethod
from enum import Enum
from dataclasses import dataclass, field
from typing import Optional
from datetime import datetime
import uuid
import math
# --- Location helper ---
@dataclass
class Location:
lat: float
lng: float
def distance_to(self, other: "Location") -> float:
# Simplified: treat lat/lng as flat plane; enough for LLD
return math.sqrt((self.lat - other.lat) ** 2 + (self.lng - other.lng) ** 2) * 111 # km
# --- Users ---
class User:
def __init__(self, user_id: str, name: str, phone: str):
self.user_id = user_id
self.name = name
self.phone = phone
class Rider(User):
pass
class VehicleType(Enum):
MINI = "mini"
SEDAN = "sedan"
SUV = "suv"
@dataclass
class Vehicle:
plate: str
type: VehicleType
class Driver(User):
def __init__(self, user_id, name, phone, vehicle: Vehicle, location: Location):
super().__init__(user_id, name, phone)
self.vehicle = vehicle
self.location = location
self.is_available = True
self.rating = 5.0
# --- Trip state ---
class TripState(ABC):
@abstractmethod
def name(self) -> str: pass
class Requested(TripState):
def name(self): return "REQUESTED"
class DriverAssigned(TripState):
def name(self): return "DRIVER_ASSIGNED"
class Started(TripState):
def name(self): return "STARTED"
class Completed(TripState):
def name(self): return "COMPLETED"
class Cancelled(TripState):
def name(self): return "CANCELLED"
# --- Trip ---
@dataclass
class Trip:
id: str
rider: Rider
source: Location
destination: Location
vehicle_type: VehicleType
state: TripState = field(default_factory=Requested)
driver: Optional[Driver] = None
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
fare: Optional[float] = None
rider_rating_of_driver: Optional[int] = None
driver_rating_of_rider: Optional[int] = None
# --- Matching strategy ---
class MatchingStrategy(ABC):
@abstractmethod
def select(self, trip: Trip, drivers: list[Driver]) -> Optional[Driver]: pass
class NearestDriverStrategy(MatchingStrategy):
def select(self, trip: Trip, drivers):
candidates = [d for d in drivers
if d.is_available and d.vehicle.type == trip.vehicle_type]
if not candidates: return None
return min(candidates, key=lambda d: d.location.distance_to(trip.source))
class HighestRatedStrategy(MatchingStrategy):
"""Among nearby drivers, pick the highest-rated."""
def __init__(self, radius_km: float = 5):
self.radius_km = radius_km
def select(self, trip, drivers):
candidates = [
d for d in drivers
if d.is_available and d.vehicle.type == trip.vehicle_type
and d.location.distance_to(trip.source) <= self.radius_km
]
if not candidates: return None
return max(candidates, key=lambda d: d.rating)
# --- Pricing strategy ---
class PricingStrategy(ABC):
@abstractmethod
def calculate(self, trip: Trip, surge: float) -> float: pass
class DistanceBasedPricing(PricingStrategy):
BASE_FARE = {VehicleType.MINI: 50, VehicleType.SEDAN: 80, VehicleType.SUV: 120}
PER_KM = {VehicleType.MINI: 10, VehicleType.SEDAN: 15, VehicleType.SUV: 20}
def calculate(self, trip, surge):
distance = trip.source.distance_to(trip.destination)
fare = self.BASE_FARE[trip.vehicle_type] + self.PER_KM[trip.vehicle_type] * distance
return round(fare * surge, 2)
class SurgeCalculator:
"""Computes surge based on supply/demand. Stub for interview."""
def current_multiplier(self, pickup: Location, available_drivers: int) -> float:
if available_drivers < 2: return 2.0
if available_drivers < 5: return 1.3
return 1.0
# --- Payment ---
class PaymentMethod(ABC):
@abstractmethod
def charge(self, amount: float, user: User) -> bool: pass
class CardPayment(PaymentMethod):
def charge(self, amount, user):
print(f"Charging ₹{amount} on {user.name}'s card")
return True
class UPIPayment(PaymentMethod):
def charge(self, amount, user):
print(f"Charging ₹{amount} via UPI to {user.name}")
return True
class CashPayment(PaymentMethod):
def charge(self, amount, user):
print(f"{user.name} to pay ₹{amount} cash to driver")
return True
# --- Ride Manager (Facade) ---
class RideManager:
def __init__(self,
matching: MatchingStrategy,
pricing: PricingStrategy,
surge: SurgeCalculator):
self.drivers: dict[str, Driver] = {}
self.trips: dict[str, Trip] = {}
self.matching = matching
self.pricing = pricing
self.surge = surge
def register_driver(self, d: Driver):
self.drivers[d.user_id] = d
def request_ride(self, rider: Rider, source: Location, destination: Location,
vehicle_type: VehicleType) -> Optional[Trip]:
trip = Trip(
id=str(uuid.uuid4())[:8],
rider=rider, source=source, destination=destination,
vehicle_type=vehicle_type,
)
self.trips[trip.id] = trip
# Match
driver = self.matching.select(trip, list(self.drivers.values()))
if not driver:
print(f"No drivers available for {vehicle_type.value}")
trip.state = Cancelled()
return trip
trip.driver = driver
driver.is_available = False
trip.state = DriverAssigned()
print(f"Trip {trip.id}: Assigned {driver.name} to {rider.name}")
return trip
def start_trip(self, trip_id: str):
trip = self.trips[trip_id]
if not isinstance(trip.state, DriverAssigned):
print(f"Can't start: trip is {trip.state.name()}")
return
trip.state = Started()
trip.started_at = datetime.now()
print(f"Trip {trip_id}: Started")
def complete_trip(self, trip_id: str, payment: PaymentMethod):
trip = self.trips[trip_id]
if not isinstance(trip.state, Started):
print(f"Can't complete: trip is {trip.state.name()}")
return
trip.state = Completed()
trip.completed_at = datetime.now()
surge = self.surge.current_multiplier(
trip.source,
sum(1 for d in self.drivers.values() if d.is_available),
)
trip.fare = self.pricing.calculate(trip, surge)
print(f"Trip {trip_id}: Completed. Fare: ₹{trip.fare} (surge x{surge})")
payment.charge(trip.fare, trip.rider)
trip.driver.is_available = True
trip.driver.location = trip.destination
def cancel_trip(self, trip_id: str):
trip = self.trips[trip_id]
if isinstance(trip.state, (Completed, Cancelled)):
print(f"Can't cancel: trip already {trip.state.name()}")
return
# Cancellation fees could depend on current state
fee = 0
if isinstance(trip.state, DriverAssigned):
fee = 30
elif isinstance(trip.state, Started):
fee = 100
trip.state = Cancelled()
if trip.driver:
trip.driver.is_available = True
print(f"Trip {trip_id}: Cancelled. Fee: ₹{fee}")
def rate_trip(self, trip_id: str, by_rider: Optional[int] = None, by_driver: Optional[int] = None):
trip = self.trips[trip_id]
if not isinstance(trip.state, Completed):
print("Can only rate completed trips")
return
if by_rider is not None:
trip.rider_rating_of_driver = by_rider
# update driver's average rating (simplified)
trip.driver.rating = (trip.driver.rating + by_rider) / 2
if by_driver is not None:
trip.driver_rating_of_rider = by_driver
# --- Usage ---
mgr = RideManager(
matching=NearestDriverStrategy(),
pricing=DistanceBasedPricing(),
surge=SurgeCalculator(),
)
# Register drivers
d1 = Driver("d1", "Raj", "9999", Vehicle("KA01-AA", VehicleType.SEDAN), Location(12.97, 77.59))
d2 = Driver("d2", "Sita", "8888", Vehicle("KA02-BB", VehicleType.SEDAN), Location(12.96, 77.58))
d3 = Driver("d3", "Ahmed", "7777", Vehicle("KA03-CC", VehicleType.MINI), Location(12.98, 77.60))
for d in [d1, d2, d3]: mgr.register_driver(d)
# Create a rider and request
rider = Rider("r1", "Amit", "6666")
trip = mgr.request_ride(rider, Location(12.97, 77.59), Location(12.95, 77.55), VehicleType.SEDAN)
mgr.start_trip(trip.id)
mgr.complete_trip(trip.id, UPIPayment())
mgr.rate_trip(trip.id, by_rider=5, by_driver=4)
Step 7 — Extensions & Tradeoffs
- Real geolocation — Haversine formula or a geo index (quadtree, geohash, H3) for efficient nearest-driver queries.
- Driver pool scalability — in-memory list is fine for LLD; real system uses a geospatial index (Redis GEO, PostGIS).
- Event-driven architecture — request, driver acceptance, pickup, drop-off become events on a queue. Observer pattern on top.
- Cancellation policy refinement — externalize as
CancellationPolicystrategy. - Driver matching with preferences — e.g., prefer drivers with matching language, female driver option — another layer of strategies or filters.
- Payment retries and failures — introduce
PaymentResultand retry logic. - Scheduled / outstation trips — extend
Tripwith scheduling fields; different state machine. - Trip history, receipts — repository pattern for persistence.
Common Pitfalls
- Putting matching logic inside
RideManager— violates SRP and OCP. Externalize as Strategy. - Hardcoding fare formulas — same issue. Strategy fixes it.
- Forgetting trip states — State pattern is natural here; without it, you get
if/elifeverywhere. - Having one
Userclass with a flagis_driver— works, but loses polymorphism; interviewers prefer a class per role. - Coupling
Tripto payment — trip and payment should be separable concerns; manager orchestrates. - Ignoring cancellation — a realistic system needs cancellation at multiple states with different rules.
Patterns Used
- State —
TripStatelifecycle. - Strategy — matching, pricing, payment method.
- Facade —
RideManager. - Factory (could be introduced) — for creating payment methods by type string.
- Observer (extension) — for event-driven UI updates.
SOLID Checks
- SRP ✓ — Matching, pricing, surge, payment, manager — all distinct concerns.
- OCP ✓ — New vehicle types, new matching/pricing/payment strategies — all additive.
- LSP ✓ — All strategy variants substitutable.
- ISP ✓ — Narrow interfaces.
- DIP ✓ —
RideManagerdepends on abstractions.
10. Meta-Patterns Across All Six Problems
Look back at the six problems. Patterns keep appearing for the same reasons. Internalizing these meta-patterns lets you recognize design choices quickly in any LLD problem.
Meta-Pattern 1 — “Pluggable Algorithm” → Strategy
Almost every problem has at least one. Parking has PricingStrategy. Elevator has SchedulingStrategy. Library has MembershipPolicy. Vending machine has change-giving (implicit). Splitwise has SplitStrategy. Ride-hailing has MatchingStrategy, PricingStrategy, PaymentMethod.
Trigger: Is there an operation that might reasonably be done in more than one way? → Strategy.
Meta-Pattern 2 — “Lifecycle with Distinct Behaviors” → State
Problems where an entity transitions through named stages, each with different allowed actions, scream State: Elevator (Idle/Moving/DoorOpen), Vending Machine (Idle/HasMoney/Dispensing), Trip (Requested/Assigned/Started/Completed).
Trigger: Does an entity behave differently based on where it is in its lifecycle, with strict transition rules? → State.
Meta-Pattern 3 — “Orchestrator of Many Parts” → Facade
Every problem has a top-level coordinator. ParkingLot, Library, VendingMachine, ExpenseManager, RideManager. Each exposes a simple API and internally coordinates sub-components.
Trigger: Is the system made of several moving parts that clients shouldn’t juggle directly? → Facade.
Meta-Pattern 4 — “Notify Interested Parties” → Observer
Parking’s DisplayBoard watching Floor. Library’s reservation notifications. Ride-hailing’s driver receiving ride requests.
Trigger: When X happens, does Y need to know, without X knowing Y explicitly? → Observer.
Meta-Pattern 5 — “Is-a Hierarchy with Behavioral Variance” → Inheritance + Polymorphism
Vehicle → Bike/Car/Truck. User → Rider/Driver. Member → Student/Faculty/Guest (though we used Strategy for their policies, which is often cleaner).
Trigger: Do I have related types with genuinely different behaviors or attributes? → Inheritance.
Meta-Pattern 6 — “Depend on Abstraction, Inject Concrete” → DIP
Every orchestrator in these problems receives strategies via its constructor rather than newing them up internally. This enables testing (inject a fake), swapping implementations, and configuration.
Trigger: Always. You should almost always inject dependencies rather than hardcoding them.
How to Use This Meta-View in Interviews
When you hear a new LLD problem, run through these triggers:
- What varies in algorithm? → Strategy candidates.
- What has a lifecycle? → State candidates.
- What’s the top-level orchestrator? → Facade.
- Who watches what? → Observer candidates.
- What are my main noun hierarchies? → Inheritance.
- Where should I inject vs construct? → DIP everywhere.
Most LLD problems will hit 3-5 of these. Hitting all six is unusual but impressive.
11. Final Interview Tips
Before the Interview
- Study the six problems in this document thoroughly. Most actual LLD interview questions are variations or combinations of these.
- Practice verbalizing your thinking. Design well internally, but also articulate well externally. A great design poorly explained is a mediocre interview.
- Practice on paper or a whiteboard without running code. You won’t have
pythonto validate syntax — you need to write clean code from memory. - Memorize the 6-step framework. Clarify → Entities → Actions → Relationships → SOLID/Patterns → Code → Extensions.
- Know your language well. Type hints, ABCs, dataclasses, enums — these show polish.
During the Interview
- Always start by clarifying. If you dive into code without clarifying, the interviewer will steer you back — that’s wasted time.
- Speak constantly. Silence is the enemy. Even “I’m considering X vs Y; I’ll go with X because…” is valuable.
- Write clean code, but not complete code. Sketch the main classes fully; use method stubs for secondary ones. Say “I’ll stub this; the logic would be…”.
- Explicitly call out patterns and SOLID. “I’m using Strategy here for pricing because…” — interviewers love this narration.
- Handle pushback gracefully. If the interviewer pokes at your design, they’re probably hinting at a problem. Listen, reconsider, adjust.
- Stay flexible. If you over-designed, admit it: “Actually, for this scope, a simple dict would be clearer than a full factory.” That’s a win, not a loss.
Common Follow-Up Questions
Interviewers often probe with:
- “How would you test this?” → Talk about unit tests for each strategy/state; mock the DB/repository; test the orchestrator with fakes. Show you understand dependency injection makes testing easy.
- “How would this scale to 1 million users?” → Move in-memory maps to a database; introduce caching (Redis); shard by user_id; async processing via queues.
- “What if X was distributed?” → Mention event-driven architecture, idempotency, eventual consistency.
- “Are there concurrency issues?” → Identify critical sections; use locks or transactional boundaries; mention optimistic vs pessimistic locking.
- “What patterns did you use and why?” → Be ready to articulate not just what pattern, but what problem it solved in your design.
- “What would you change?” → Always have at least one self-identified improvement. Shows self-awareness.
Pitfalls Interviewers Specifically Look For
From the interviewer’s perspective, red flags include:
- Writing code before clarifying.
- One giant class handling everything.
- Hardcoded strategies (
if type == "premium":). - No inheritance or polymorphism when they’d obviously fit.
- No state pattern where a clear lifecycle exists.
- Ignoring the interviewer’s hints.
- Being unable to explain any design choice.
- Perfect code but zero design thinking.
The Three Things You Must Demonstrate
Regardless of the problem, by the end of the interview you should have clearly demonstrated:
- Structured thinking — a clean, deliberate decomposition of the problem.
- Design fluency — natural use of SOLID and a few well-chosen patterns.
- Communication — explaining your reasoning and handling feedback well.
If you nail these three, the specific problem almost doesn’t matter.
Final Thought
Designing software is a craft. Patterns, principles, and frameworks like the ones in this series are the tools of the craft — but craftsmanship is what emerges when you apply them with judgment, restraint, and a clear eye for the problem in front of you.
You’ve walked through OOP fundamentals, SOLID principles, 23 design patterns, and six realistic interview designs. You now have the vocabulary and the judgment to approach any LLD problem with confidence.
Go practice. Design things. Build things. Break them. Refactor them. That’s where real mastery comes from.
Good luck in your interviews — you’ve got the toolkit. Now make it yours.