LLD Cohort · Week 7 · Class 1 · recording 2h52m
Parking Lot & Elevator System
Two classic LLD interview problems. The Parking Lot is walked through from requirements to entities, relationships and working code. The Elevator System is live-coded as a multi-threaded design, with one thread per lift, two heaps and the SCAN algorithm. (The DSA hour at the start of the recording, 0:00 – 1:00, is not covered in these notes.)
Each topic: one-line idea → the problem → real-life picture → the design step by step → Python code → doubts & corrections → remember
LLD: Parking Lot ▶ 1:00:51 – 1:41:50
A central ParkingLot owns floors → spots and the active tickets. Entry and exit gates are thin APIs on top of it. Fees and payments are pluggable Strategies, and a lock makes spot assignment thread-safe.
The problem
In the interview you get a blank page and the words "design a parking lot". The teacher's advice is to fall back on the framework every time you feel stuck: (1) functional + non-functional requirements → (2) core entities and relationships → (3) code. Start with a one-line definition so you never lose the thread: "A parking lot is a place where people park vehicles of different types." Then brain-dump nouns (slots, vehicle types, entry/exit gates, ticket, floors, manager) before you structure anything.
Requirements
| Functional | Non-functional |
|---|---|
| Users can park and unpark their vehicle. | Thread safety: two gates must never assign the same slot to two vehicles. |
| The lot manages multiple floors, and a floor has multiple slots of different types. | Extensibility: add new pricing, payment methods, gates, slot types or vehicle types without rewrites. |
| The entry gate generates a ticket and allocates a slot. | Scalable / low latency (the class debated whether the lot should be a Singleton). |
| The exit gate processes payment and vacates the slot. | |
| Supports multiple vehicle types and slot types, plus different fee strategies (hourly, fixed) and payment strategies (UPI, card). |
Mention design patterns early, even ones you may not have time to implement ("different strategies for fee calculation"). It tells the interviewer right away that you know LLD.
Core entities and relationships
- Vehicle (number,
VehicleTypeenum: BIKE / CAR / TRUCK). - ParkingSpot (id,
SpotTypeSMALL / MEDIUM / LARGE, the parked Vehicle object) withis_free(),can_fit(),park(),unpark(). - ParkingFloor has a list of spots and
find_spot(vehicle). - ParkingLot (the "manager"/orchestrator) has a list of floors and a map of active tickets keyed by vehicle number.
- Ticket: id, vehicle, spot, entry time, exit time.
- EntryGate issues tickets. ExitGate owns the FeeStrategy and PaymentStrategy and releases the slot.
The class named the relationships. "Lot has floors" and "floor has spots" are has-a relationships. The teacher first accepted "association" and then refined it to aggregation. He stressed that aggregation is not about having a list: holding even one object can be aggregation.
Run the use case before coding
Walk the flow once, out loud, before writing code. It is how you catch a wrong entity early, instead of discovering halfway through coding that "it should have been the entry gate, not the manager":
- The vehicle asks the ParkingLot (via the entry gate) to be parked.
- The lot loops over the floors and asks each floor for a free spot that fits.
- The spot parks the vehicle, a ticket is created and stored in active tickets, and the ticket goes back to the driver.
- At exit: find the ticket → mark the exit time → the fee strategy computes the amount → the payment strategy collects it → the spot is unparked and the ticket removed.
The gates are the doors of a building, but the building's control room (the ParkingLot) decides everything. When a door opens, it is because the control room told it to. That is why the gate keeps a reference to the lot and only forwards calls. The teacher's advice: "Don't think emotionally about which object is 'first'. Pick a sensible orchestrator and defend your design."
Python code
import itertools
import threading
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from typing import Optional
# ---------- enums ----------
class VehicleType(Enum):
BIKE = 1
CAR = 2
TRUCK = 3
class SpotType(Enum): # SMALL fits a bike, MEDIUM a car, LARGE a truck
SMALL = 1
MEDIUM = 2
LARGE = 3
FITS = {VehicleType.BIKE: SpotType.SMALL,
VehicleType.CAR: SpotType.MEDIUM,
VehicleType.TRUCK: SpotType.LARGE}
@dataclass
class Vehicle:
number: str
type: VehicleType
# ---------- spot & floor ----------
class ParkingSpot:
def __init__(self, spot_id: str, spot_type: SpotType):
self.id, self.type = spot_id, spot_type
self.vehicle: Optional[Vehicle] = None # the real car sits here, not just its id
def is_free(self) -> bool:
return self.vehicle is None
def can_fit(self, v: Vehicle) -> bool:
return self.is_free() and FITS[v.type] == self.type
def park(self, v: Vehicle) -> None:
self.vehicle = v
def unpark(self) -> None:
self.vehicle = None
class ParkingFloor:
def __init__(self, floor_id: int, spots: list[ParkingSpot]):
self.id, self.spots = floor_id, spots
def find_spot(self, v: Vehicle) -> Optional[ParkingSpot]:
for spot in self.spots: # simple scan is fine in an interview
if spot.can_fit(v):
return spot
return None
# ---------- ticket ----------
@dataclass
class Ticket:
id: int
vehicle: Vehicle
spot: ParkingSpot
entry_time: datetime = field(default_factory=datetime.now)
exit_time: Optional[datetime] = None
def mark_exit(self, when: Optional[datetime] = None) -> None:
self.exit_time = when or datetime.now()
def duration(self) -> timedelta:
return (self.exit_time or datetime.now()) - self.entry_time
# ---------- strategies ----------
class FeeStrategy(ABC):
@abstractmethod
def calculate(self, ticket: Ticket) -> float: ...
class HourlyFee(FeeStrategy):
RATE = {VehicleType.BIKE: 10, VehicleType.CAR: 20, VehicleType.TRUCK: 50}
def calculate(self, ticket: Ticket) -> float:
hours = max(1, -(-int(ticket.duration().total_seconds()) // 3600)) # round up, min 1h
return hours * self.RATE[ticket.vehicle.type]
class FixedFee(FeeStrategy):
def calculate(self, ticket: Ticket) -> float:
return 50.0
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount: float) -> None: ...
class UpiPayment(PaymentStrategy):
def pay(self, amount: float) -> None:
print(f"Paid Rs {amount:.0f} via UPI")
# ---------- the orchestrator ----------
class ParkingLot:
def __init__(self):
self.floors: list[ParkingFloor] = []
self.active_tickets: dict[str, Ticket] = {} # vehicle number -> ticket
self._ids = itertools.count(1)
self._lock = threading.Lock() # two gates must never get the same spot
def add_floor(self, floor: ParkingFloor) -> None:
self.floors.append(floor)
def assign_spot(self, v: Vehicle) -> Optional[Ticket]:
with self._lock: # find + park is ONE atomic step
for floor in self.floors:
spot = floor.find_spot(v)
if spot:
spot.park(v)
ticket = Ticket(next(self._ids), v, spot)
self.active_tickets[v.number] = ticket
return ticket
return None
def release(self, vehicle_number: str) -> Ticket:
with self._lock:
ticket = self.active_tickets.pop(vehicle_number)
ticket.spot.unpark()
return ticket
# ---------- gates: thin API layers over the lot ----------
class EntryGate:
def __init__(self, lot: ParkingLot):
self.lot = lot
def enter(self, v: Vehicle) -> Optional[Ticket]:
ticket = self.lot.assign_spot(v)
print(f"Ticket {ticket.id}: {v.number} -> spot {ticket.spot.id}" if ticket else "Lot full")
return ticket
class ExitGate:
def __init__(self, lot: ParkingLot, fee: FeeStrategy, payment: PaymentStrategy):
self.lot, self.fee, self.payment = lot, fee, payment
def exit(self, ticket: Ticket) -> float:
ticket.mark_exit()
amount = self.fee.calculate(ticket)
self.payment.pay(amount)
self.lot.release(ticket.vehicle.number) # vacate the slot + drop active ticket
return amount
# usage
lot = ParkingLot()
lot.add_floor(ParkingFloor(1, [ParkingSpot("1-S1", SpotType.SMALL),
ParkingSpot("1-M1", SpotType.MEDIUM)]))
entry = EntryGate(lot)
exit_gate = ExitGate(lot, HourlyFee(), UpiPayment())
t = entry.enter(Vehicle("KA-01-1234", VehicleType.CAR)) # Ticket 1: KA-01-1234 -> spot 1-M1
entry.enter(Vehicle("KA-02-9999", VehicleType.CAR)) # Lot full (only one medium spot)
exit_gate.exit(t) # Paid Rs 20 via UPI"You don't throw your key into the parking spot. You put the car there." The spot really contains the vehicle, so modelling it as an object reference matches the domain.
The teacher's Java code returned Optional<ParkingSpot> from findSpot, which forces callers to check isPresent() instead of hitting a NullPointerException. In Python, write the return type as Optional[ParkingSpot] and return None. In Java the quick thread-safety fix is synchronized on assignSpot. In Python it is a threading.Lock around "find + park", which must be one atomic step.
UML
classDiagram
class ParkingLot {
+floors
+active_tickets
+assign_spot(vehicle) Ticket
+release(vehicle_number) Ticket
}
class ParkingFloor {
+id
+spots
+find_spot(vehicle) ParkingSpot
}
class ParkingSpot {
+id
+type
+vehicle
+is_free() bool
+can_fit(vehicle) bool
+park(vehicle)
+unpark()
}
class Vehicle {
+number
+type
}
class Ticket {
+id
+entry_time
+exit_time
+mark_exit()
+duration()
}
class EntryGate {
+enter(vehicle) Ticket
}
class ExitGate {
+exit(ticket) float
}
class FeeStrategy {
+calculate(ticket) float
}
class PaymentStrategy {
+pay(amount)
}
ParkingLot o-- ParkingFloor
ParkingFloor o-- ParkingSpot
ParkingSpot --> Vehicle
ParkingLot --> Ticket
Ticket --> ParkingSpot
Ticket --> Vehicle
EntryGate --> ParkingLot
ExitGate --> ParkingLot
ExitGate --> FeeStrategy
ExitGate --> PaymentStrategy
HourlyFee ..|> FeeStrategy
FixedFee ..|> FeeStrategy
UpiPayment ..|> PaymentStrategy
Design patterns used and why
| Pattern | Where | Why |
|---|---|---|
| Strategy | FeeStrategy, PaymentStrategy | Hourly vs fixed pricing, UPI vs card. New rules plug in without touching ExitGate. |
| Facade / orchestrator | ParkingLot | One central place that owns floors and tickets. Gates are thin API layers. |
| Singleton (debated) | ParkingLot | There is one physical lot, but singletons hurt testing. Mention it and justify your choice either way. |
Pitfalls, follow-ups and extensions mentioned
- Concurrency is the real test. A student argued that Parking Lot is mostly about entities. The teacher disagreed: put yourself in a spot where you have to think about concurrency. Mentioning thread safety gets you "50% there". Saying "we can make it more granular with per-spot locks" shows depth.
- Linear scan over spots is fine in an interview. Optimisations (per-type free lists) are follow-ups.
- The teacher's reference code is "not perfect: synchronisation isn't handled", but it runs end to end. Aim to reach a working flow, not just "two classes and three interfaces".
- Alternative design: the gate itself does the parking. Both are fine if you can defend the relationships.
→
Floors can have different layouts and spot counts, and the floor id matters (which floor to send the car to). Lot → floors → spots is the common, defendable structure, but it is a design choice.
→
Use any unique key. Vehicle number is the natural one, and that is what the code uses.
→
The exit gate, because that is where the fee is calculated and paid. The exit gate also releases the slot and removes the active ticket.
- Framework: requirements (functional + non-functional) → entities and relationships → code. Come back to it whenever you are stuck.
- Walk the use case end to end before coding, so you can defend each relationship.
- Lot → Floor → Spot (aggregation). Ticket links vehicle + spot + times.
- Fee and payment are Strategies owned by the exit gate.
- Make "find a spot + park" atomic (lock /
synchronized) and say so out loud.
LLD: Elevator System (multi-threaded) ▶ 1:42:00 – 2:52:22
Each elevator is an independent thread with an up-stops min-heap and a down-stops max-heap and serves them with the SCAN algorithm. An ElevatorSystem uses a selection Strategy to pick which lift answers a hall call.
The problem
A building has several lifts. People press buttons in the hall and inside the cabin. The system must decide which lift goes where without the lifts zig-zagging ("the people inside will be scared: who wrote this algorithm?"). The teacher chose this problem to teach concurrency, because every lift runs on its own.
Lifts 1, 2 and 3 don't know or care where the others are. Each keeps its own to-do list. A controller at the front desk only decides which lift gets a new hall call. In code, that means one thread per lift plus a manager object.
How one lift works: two heaps + SCAN
- up_stops is a min-heap. Going up from floor 5 with stops {6, 8, 10}, serve 6 first. Going to 8 and then back to 6 would be a mess.
- down_stops is a max-heap. Going down from 10, serve 8 before 7.
- Which heap gets a request? Compare only with the lift's current floor:
floor > current → up_stops, otherwisedown_stops. The current direction does not matter. - Each tick: if the top of the current heap equals the current floor, stop (open/close doors) and pop it. Otherwise move one floor. When the current direction's heap is empty, switch direction. If both heaps are empty, go
IDLE. - This is SCAN: keep going one way until there is no work left that way, then reverse. No mid-way direction changes. (Strictly, a lift that stops at the last request rather than the top floor is the LOOK variant, which is the optimisation the teacher described: "if requests only go up to 50 of 100 floors, don't go to 100".)
Hall call vs cabin call. A request is just "open the door at floor X". A hall button (floor 5) first goes to the system, which picks the best lift and adds floor 5 to that lift's heaps. Once inside, pressing 11 goes directly to that lift. No selection is needed, and 11 > 5 puts it in up_stops. Treating both the same way keeps the lift logic simple.
Requirements
| Functional | Non-functional |
|---|---|
| The system manages multiple elevators. | Thread-safe. Each elevator runs in its own thread (say this out loud). |
| Users can request a lift from any floor (hall) and from inside the lift (cabin). | Extensible: new selection algorithms. |
| Uses a pluggable elevator selection strategy. | Reliable: edge cases around security, power, doors and weight (out of scope, but mention them). |
| Lifts don't move to and fro: requests are processed with SCAN. | |
| Out of scope, cover if time permits: floor objects, internal/external request types, admin controller, emergency handling. |
Write what the system should be able to do ("system should process requests from users"), not a list of actors. Listing out-of-scope items shows design thinking without spending time on them.
Core entities
ElevatorSystem (manager), Elevator (runnable/thread), Request (just a floor id), Direction enum (UP / DOWN / IDLE; a state like OUT_OF_SERVICE could be added), ElevatorSelectionStrategy with NearestElevatorStrategy. Another idea from class is "empty-first, else nearest". A Floor would only hold elevator doors, so it is skipped.
Building it step by step (as live-coded)
Request(floor_id), deliberately without a source floor or a user.Elevator implements Runnable: id, current floor, two priority queues (Java:new PriorityQueue<>()and(a, b) -> b - afor the max-heap), andadd_request()with the "greater than current → up" rule.run(): a loop that processes the up heap while moving up, switches direction when it is empty, then processes the down heap.move()doescurrent += 1or-= 1based on direction (a student suggested setting the direction inside move).ElevatorSystem: creates N elevators and keeps their threads (so it can stop them), callsthread.start()(which callsrun()), andrequest_elevator()=strategy.select()thenlift.add_request().NearestElevatorStrategyis the "find the minimum in an array" pattern over|lift.floor − request.floor|.- A
runningflag set to false bystop()ends each thread cleanly.wait/notifywere promised for the next class. The code below already uses them so idle lifts sleep instead of spinning.
Python code
import heapq
import threading
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
TICK = 0.05 # seconds to travel one floor (fast, for the demo)
class Direction(Enum):
UP = "UP"
DOWN = "DOWN"
IDLE = "IDLE"
@dataclass(frozen=True)
class Request:
floor_id: int # "open the door at this floor" - hall button OR cabin button
class Elevator:
"""One lift = one thread. It knows nothing about the other lifts."""
def __init__(self, elevator_id: int):
self.id = elevator_id
self.current_floor = 0
self.direction = Direction.IDLE
self.up_stops: list[int] = [] # min-heap: going up, nearest higher floor first
self.down_stops: list[int] = [] # max-heap (store -floor): going down, nearest lower first
self._cv = threading.Condition() # a lock + wait/notify in one object
self._running = True
# ----- called by other threads (the system / passengers) -----
def add_request(self, req: Request) -> None:
with self._cv:
if req.floor_id > self.current_floor:
heapq.heappush(self.up_stops, req.floor_id)
elif req.floor_id < self.current_floor:
heapq.heappush(self.down_stops, -req.floor_id)
else:
print(f"Elevator {self.id} opens doors at {self.current_floor}")
self._cv.notify() # wake the lift if it was sleeping (idle)
def stop(self) -> None:
with self._cv:
self._running = False
self._cv.notify()
# ----- the lift's own thread -----
def run(self) -> None:
while True:
with self._cv:
while self._running and not self.up_stops and not self.down_stops:
self.direction = Direction.IDLE
self._cv.wait() # sleep instead of busy-spinning
if not self._running:
return
self._step()
time.sleep(TICK) # travel time, outside the lock
def _step(self) -> None:
# SCAN: keep the current direction while it has work, else switch.
if self.direction != Direction.DOWN and self.up_stops:
self.direction = Direction.UP
elif self.down_stops:
self.direction = Direction.DOWN
else:
self.direction = Direction.UP
if self.direction == Direction.UP:
if self.up_stops[0] == self.current_floor:
heapq.heappop(self.up_stops)
print(f"Elevator {self.id} stopped at {self.current_floor}")
else:
self.current_floor += 1
else:
if -self.down_stops[0] == self.current_floor:
heapq.heappop(self.down_stops)
print(f"Elevator {self.id} stopped at {self.current_floor}")
else:
self.current_floor -= 1
# ----- strategy: which lift serves a hall call? -----
class ElevatorSelectionStrategy(ABC):
@abstractmethod
def select(self, elevators: list[Elevator], req: Request) -> Elevator: ...
class NearestElevatorStrategy(ElevatorSelectionStrategy):
def select(self, elevators, req):
# "find the minimum in an array" - smallest distance wins
return min(elevators, key=lambda e: abs(e.current_floor - req.floor_id))
class ElevatorSystem:
def __init__(self, count: int, strategy: ElevatorSelectionStrategy):
self.strategy = strategy
self.elevators = [Elevator(i) for i in range(count)]
self.threads = [threading.Thread(target=e.run, daemon=True) for e in self.elevators]
for t in self.threads: # keep the threads so we can stop/join them later
t.start()
def request_elevator(self, req: Request) -> Elevator:
"""Hall button: system picks the best lift, then hands it the request."""
lift = self.strategy.select(self.elevators, req)
print(f"Assigning elevator {lift.id} for floor {req.floor_id}")
lift.add_request(req)
return lift
def shutdown(self) -> None:
for e in self.elevators:
e.stop()
for t in self.threads:
t.join()
# usage
system = ElevatorSystem(2, NearestElevatorStrategy())
lift = system.request_elevator(Request(5)) # someone presses the hall button on floor 5
time.sleep(0.4)
lift.add_request(Request(2)) # inside the cabin they press 2 (no selection needed)
time.sleep(0.4)
system.shutdown()
# Assigning elevator 0 for floor 5
# Elevator 0 stopped at 5
# Elevator 0 stopped at 2Java: class Elevator implements Runnable + new Thread(elevator).start(). Python: threading.Thread(target=elevator.run).start(). Java's PriorityQueue takes a comparator, and (a, b) -> b - a gives a max-heap. Python's heapq is min-only, so push -floor for the max-heap. Java's wait()/notify() inside synchronized maps to Python's threading.Condition (cv.wait() / cv.notify() inside with cv:).
UML
classDiagram
class ElevatorSystem {
+elevators
+threads
+request_elevator(request) Elevator
+shutdown()
}
class Elevator {
+id
+current_floor
+direction
+up_stops
+down_stops
+add_request(request)
+run()
+stop()
}
class Request {
+floor_id
}
class Direction {
UP
DOWN
IDLE
}
class ElevatorSelectionStrategy {
+select(elevators, request) Elevator
}
class NearestElevatorStrategy
ElevatorSystem *-- Elevator
ElevatorSystem --> ElevatorSelectionStrategy
NearestElevatorStrategy ..|> ElevatorSelectionStrategy
Elevator --> Direction
Elevator ..> Request
Design patterns used and why
- Strategy for elevator selection (nearest, empty-first, least-used). "Strategy is everywhere. Pitch it first." Decorator and State are next in line if time permits.
- Thread-per-object (active object): each
Elevatorowns its loop and data, and other threads only enqueue requests. - Observer came up as an aside: a government emergency alert reached every phone during class, which is "notification + observer".
Follow-ups and extensions mentioned
- Use a thread-safe
BlockingQueueandwait/notifyto make it properly thread-safe (next class). - Smarter strategies that avoid disturbing a lift that is already busy, and capacity/weight limits.
- Elevator state (RUNNING, IDLE, OUT_OF_SERVICE) → State pattern.
- Homework: finish the code and run a simulation with several lifts and requests spaced a few seconds apart.
→
Not in this design. A hall press at floor 5 is "open at 5" (handled by the system, which picks a lift). The cabin press of 11 is "open at 11" (sent straight to that lift). From the lift's point of view, both are just floors to stop at.
→
7 is not greater than 10, so it goes to down_stops. It is served when the lift travels downwards.
→
No. The user is represented by a Request, which is its own entity, and there is also the system/manager and the strategy. Floors are kept as plain ids for simplicity.
- One thread per elevator. Lifts are independent and only the system chooses between them.
- Up stops = min-heap, down stops = max-heap. Route a request by comparing it with the current floor.
- SCAN/LOOK: finish one direction, then switch. Go IDLE when both heaps are empty.
- Selection algorithm = Strategy. Nearest lift = argmin of distance.
- Keep the thread references so you can stop the system cleanly.