Design an Elevator System
An office tower has a bank of elevators serving the same floors. People on a floor press up or down (a hall call); people inside a car press their destination (a car call). A controller decides which car answers each hall call, and every car decides the order in which it visits its stops.
Riders care about waiting time; the building manager cares about safety. Doors must never close on someone, a car must refuse to move when overloaded, and a car can be taken out of service for maintenance without breaking the rest of the bank.
Design the classes for the controller and the cars. In 45 minutes, agree on scope, model the car as a state machine, choose a scheduling approach for a single car and a dispatch policy for the bank, and code a tick-based simulation that shows calls being served. Explain how you would swap in a smarter dispatcher later.
Use the LLD board for your class diagram and the LLD code tab to write and run your classes.
Design Path · Low-Level Design · problem 18 of 18
Ask before you design. Try answering each yourself, then compare.
- How many cars and floors?
Typical answer
A handful of cars (3 to 8) over up to about 50 floors, all serving every floor.
- Do riders pick their destination before boarding (destination dispatch)?
Typical answer
No. Classic up/down hall buttons plus floor buttons inside each car.
- Is a hall call assigned to a car once, or re-evaluated?
Typical answer
Assign once at call time for this version; re-assignment is a follow-up.
- Which safety features matter?
Typical answer
Door obstruction sensor, overload detection, and a maintenance mode that takes a car out of dispatch.
- Should we simulate real time and physics?
Typical answer
No. A discrete tick (one floor of travel or one door cycle) is enough.
- What should the system optimise?
Typical answer
Average waiting time, while never starving a floor.
- Do we need fire service or emergency modes?
Typical answer
Mention how they would fit; do not implement them.
Requirements
Functional
- Accept hall calls (floor + direction) and assign each to one in-service car.
- Accept car calls (destination floor) from riders inside a car.
- Each car serves its stops in an order that avoids needless reversals.
- Doors stay open while the doorway is obstructed or the car is overloaded.
- Cars can enter and leave maintenance; cars in maintenance get no calls.
Non-functional
- Dispatch decision in under 10 ms for up to 8 cars and 50 floors.
- No floor waits unboundedly: every stop is eventually served.
- Safety rules cannot be skipped by any scheduling policy.
Out of scope
- Motor control, acceleration curves and real timing.
- Fire service, earthquake and VIP modes (discuss only).
- Destination dispatch kiosks.
Class design
Core objects. ElevatorBank is the controller: it owns the cars, receives hall calls and car calls, and advances the simulation with tick(). Each Elevator holds its floor, direction, ElevatorState, a set of pending stops, its load and a Door. A Request is a hall call (floor plus direction).
Dispatch. On a hall call the bank asks a DispatchStrategy to choose a car. The default nearest-car policy scores each in-service car by distance, adding a penalty when the car is busy and heading away or travelling the other way. The winner just gets a new stop.
Per-car scheduling. A car keeps moving in its current direction while stops remain ahead, then reverses. This sweep (LOOK) bounds waiting time, unlike always going to the nearest stop.
State machine and safety. Each tick a car either finishes a door cycle, opens at a stop, or moves one floor. Leaving DOORS_OPEN requires the load to be within capacity and Door.try_close() to succeed, so no strategy can bypass safety. Maintenance drops the car out of dispatch and hands back its stops for re-dispatch.
Extensibility. New dispatch policies (zoning, peak-hour lobby bias) are new strategy classes; special modes such as fire service become extra states.
Open the LLD board and press Reference to see the class diagram.
Patterns used
- Strategy: dispatch policy varies by building and time of day, so the bank depends on a DispatchStrategy interface.
- State: a car behaves differently when idle, moving, holding doors open or in maintenance; transitions are explicit.
- Composition: a car owns its Door, so door safety rules travel with the car rather than living in the controller.
Reference implementation
from abc import ABC, abstractmethod
from enum import Enum
class Direction(Enum):
UP = 1
DOWN = -1
IDLE = 0
class ElevatorState(Enum):
IDLE = "idle"
MOVING = "moving"
DOORS_OPEN = "doors open"
MAINTENANCE = "maintenance"
class Door:
def __init__(self):
self.is_open = False
self.obstructed = False # set by the light-curtain sensor
def open(self):
self.is_open = True
def try_close(self) -> bool:
if self.obstructed:
return False
self.is_open = False
return True
class Request:
"""A hall call: someone on `floor` pressed the up or down button."""
def __init__(self, floor: int, direction: Direction):
self.floor, self.direction = floor, direction
class Elevator:
def __init__(self, car_id: str, capacity_kg: int = 800, log=print):
self.id, self.capacity_kg, self.log = car_id, capacity_kg, log
self.floor, self.direction, self.state = 0, Direction.IDLE, ElevatorState.IDLE
self.stops, self.load_kg, self.door = set(), 0, Door()
def add_stop(self, floor: int):
if self.state is not ElevatorState.MAINTENANCE:
self.stops.add(floor)
def board(self, kg: int):
self.load_kg += kg
def alight(self, kg: int):
self.load_kg = max(0, self.load_kg - kg)
def set_maintenance(self, on: bool):
"""Taking a car out drops its stops; the bank must re-dispatch them."""
dropped, self.stops = self.stops, set()
self.state = ElevatorState.MAINTENANCE if on else ElevatorState.IDLE
return dropped if on else set()
def _next_direction(self) -> Direction:
"""LOOK scheduling: keep going while stops lie ahead, otherwise turn around."""
if not self.stops:
return Direction.IDLE
if self.direction is Direction.UP and any(f > self.floor for f in self.stops):
return Direction.UP
if self.direction is Direction.DOWN and any(f < self.floor for f in self.stops):
return Direction.DOWN
nearest = min(self.stops, key=lambda f: abs(f - self.floor))
return Direction.UP if nearest > self.floor else Direction.DOWN
def step(self):
"""Advance the car by one tick (one floor of travel or one door cycle)."""
if self.state is ElevatorState.MAINTENANCE:
return
if self.state is ElevatorState.DOORS_OPEN:
if self.load_kg > self.capacity_kg:
self.log(f" {self.id}: overload ({self.load_kg} kg), doors held open")
return
if not self.door.try_close():
self.log(f" {self.id}: doorway obstructed, doors held open")
return
self.state = ElevatorState.IDLE
self.log(f" {self.id}: doors closed at floor {self.floor}")
return
if self.floor in self.stops:
self.stops.discard(self.floor)
self.door.open()
self.state = ElevatorState.DOORS_OPEN
self.log(f" {self.id}: doors open at floor {self.floor}")
return
self.direction = self._next_direction()
if self.direction is Direction.IDLE:
self.state = ElevatorState.IDLE
return
self.floor += self.direction.value
self.state = ElevatorState.MOVING
class DispatchStrategy(ABC):
@abstractmethod
def choose(self, cars: list, req: Request) -> Elevator:
...
class NearestCarStrategy(DispatchStrategy):
"""Cheapest car by distance, with a penalty for cars heading away from the caller."""
def cost(self, car: Elevator, req: Request) -> float:
distance = abs(car.floor - req.floor)
if car.direction is Direction.IDLE or not car.stops:
return distance
heading_towards = (req.floor - car.floor) * car.direction.value >= 0
same_way = car.direction is req.direction
return distance if heading_towards and same_way else distance + 2 * len(car.stops) + 10
def choose(self, cars, req):
usable = [c for c in cars if c.state is not ElevatorState.MAINTENANCE]
return min(usable, key=lambda c: (self.cost(c, req), c.id), default=None)
class ElevatorBank:
def __init__(self, floors: int, cars: list, strategy: DispatchStrategy):
self.floors, self.cars, self.strategy = floors, {c.id: c for c in cars}, strategy
def call(self, floor: int, direction: Direction) -> str:
if not 0 <= floor < self.floors:
raise ValueError("no such floor")
car = self.strategy.choose(list(self.cars.values()), Request(floor, direction))
if car is None:
raise RuntimeError("all cars out of service")
car.add_stop(floor)
return car.id
def press(self, car_id: str, floor: int):
if 0 <= floor < self.floors:
self.cars[car_id].add_stop(floor)
def tick(self):
for car in self.cars.values():
car.step()
def status(self) -> str:
return " | ".join(f"{c.id}@{c.floor} {c.state.value}" for c in self.cars.values())
if __name__ == "__main__":
a, b, c = Elevator("A"), Elevator("B"), Elevator("C")
bank = ElevatorBank(10, [a, b, c], NearestCarStrategy())
b.floor, c.floor = 8, 1
c.set_maintenance(True)
print("start:", bank.status())
print("hall call floor 6 DOWN ->", bank.call(6, Direction.DOWN))
print("hall call floor 2 UP ->", bank.call(2, Direction.UP))
def riders_get_in():
print(" A: rider presses 7, but a suitcase blocks the door")
a.board(80); bank.press("A", 7); a.door.obstructed = True
print(" B: a crowd boards (950 kg) and presses 0")
b.board(950); bank.press("B", 0)
def problems_cleared():
print(" A: suitcase pulled in; B: two riders step out")
a.door.obstructed = False; b.alight(250)
events = {4: riders_get_in, 5: problems_cleared}
for t in range(1, 13):
if t in events:
events[t]()
bank.tick()
print(f"t={t:<2}", bank.status())
c.set_maintenance(False)
print("C back in service; hall call floor 1 UP ->", bank.call(1, Direction.UP))
Trade-offs
- Assigning a hall call once is simple and predictable, while re-scoring every tick lowers waits but can make cars flip-flop.
- A sweep order guarantees service but can carry a rider past a nearer stop; nearest-first is faster on average yet can starve floors.
- A tick simulation is deterministic and easy to test, but it hides real timing such as acceleration and door dwell.
- A single controller is easy to reason about, but it is a single point of failure that real systems back up with per-car fallbacks.
What a strong answer shows
- Separates bank-level dispatch from per-car stop ordering.
- Models car states and transitions explicitly instead of scattering boolean flags.
- Uses a sweep (LOOK/SCAN) style order and explains why it avoids starvation.
- Places safety checks inside the car so no policy can bypass them.
- Makes the dispatcher swappable and can describe a better cost function.
- Handles maintenance mode and what happens to calls already assigned.
Mistakes to avoid
- One giant controller that moves every car directly, mixing dispatch, scheduling and door logic.
- Serving the nearest stop first, which can leave the top or bottom floors waiting indefinitely.
- Ignoring direction in hall calls, so a car going down picks up someone who wants to go up.
- Treating doors and overload as an afterthought rather than guards on state transitions.
- Over-engineering physics or threads before the object model is clear.
Follow-ups to expect
- How would you reduce lobby waits at 9 a.m.? (Bias idle cars to return to the lobby; a time-of-day strategy.)
- A car goes into maintenance with assigned hall calls. What happens? (Return its stops to the bank and re-dispatch them.)
- How would you add fire service mode? (A new state that ignores hall calls and sends the car to the ground floor.)
- How do you re-assign calls when a better car frees up? (Re-score open hall calls each tick with hysteresis to avoid flapping.)
- How would you test this? (Deterministic ticks let you replay scenarios and assert on positions and wait times.)