Overview
Design an elevator system for a building with N floors and M elevator cars. Requirements: passengers press hall buttons (up/down) on a floor and car buttons (destination floor) inside a car; a controller assigns hall calls to cars; each car moves floor by floor, stops at requested floors, and opens doors; the scheduling policy should be swappable; and the system should handle maintenance mode and capacity.
The core ideas: each car keeps sorted sets of up-stops and down-stops and follows the SCAN/LOOK algorithm (keep moving in the current direction while there are stops ahead, then reverse). A dispatcher (Mediator) chooses the best car for each hall call via a strategy (nearest suitable car, least busy). The car's state machine (IDLE, MOVING_UP, MOVING_DOWN, DOORS_OPEN, MAINTENANCE) prevents illegal actions.
The bus keeps driving in one direction, picking up and dropping off along the way, and only turns around when there are no more stops ahead. A dispatcher decides which bus should pick up a new waiting passenger.
When to use it
- One of the most common LLD interview questions.
- Practicing State, Strategy, Mediator, and Observer together.
- Simulation-style problems with a clock (step function).
Where it shows up in interviews
Recognize it when: assign requests to one of many workers.
- Design an elevator system
- Design a ride dispatch system
Recognize it when: entity moves through states over time.
- Design a traffic light controller
- Design a vending machine
Where it is used in real software
Modern buildings (Otis, KONE, Schindler) ask for the destination in the lobby and group passengers going to nearby floors.
The elevator algorithm is also used by operating systems to schedule disk head movement.
Real controllers enforce door interlocks, overload sensors, and fire service modes as state rules.
Key terms
- Hall call
- Request from a floor with a direction.
- Car call
- Destination selected inside the car.
- SCAN / LOOK
- Serve all requests in one direction, then reverse.
- Dispatcher
- Assigns hall calls to cars (Mediator + Strategy).
- Tick / step
- Simulation advancing time by one unit.
How it works, step by step
- 1Clarify
Floors, cars, capacity, scheduling goal, maintenance, concurrency.
- 2Entities
Building, ElevatorCar, Direction, CarState, Request, Dispatcher, SchedulingStrategy.
- 3Car logic
upStops and downStops TreeSets; step() moves one floor or opens doors.
- 4Dispatch logic
Strategy scores cars by distance and direction compatibility.
- 5Edge cases
No available car, requests to current floor, maintenance, invalid floors.
STEP 1Direction UP. Up-stops ahead: 5 and 8. The down-stop at 2 waits.
Dispatcher scoring (nearest suitable car)
Hall call: floor 6, direction UP
| Car | Position / direction | Suitable? | Score (lower is better) |
|---|---|---|---|
| A | Floor 4, moving UP | Yes, 6 is ahead in the same direction | 2 |
| B | Floor 7, moving UP | Passed floor 6 already | Penalty: 1 + 2 x 10 |
| C | Floor 1, IDLE | Yes | 5 |
| D | MAINTENANCE | No | Excluded |
NOWCar: A | Position / direction: Floor 4, moving UP | Suitable?: Yes, 6 is ahead in the same direction | Score (lower is better): 2
Car A is assigned. Swapping the strategy (for example least-loaded) changes only one class.
Implementation
import java.util.*; enum Direction { UP, DOWN, IDLE }enum CarState { IDLE, MOVING, DOORS_OPEN, MAINTENANCE } final class ElevatorCar { private final int id, minFloor, maxFloor; private int floor; private Direction direction = Direction.IDLE; private CarState state = CarState.IDLE; private final TreeSet<Integer> upStops = new TreeSet<>(), downStops = new TreeSet<>(); ElevatorCar(int id, int minFloor, int maxFloor) { this.id = id; this.minFloor = minFloor; this.maxFloor = maxFloor; this.floor = minFloor; } int id() { return id; } int floor() { return floor; } Direction direction() { return direction; } boolean available() { return state != CarState.MAINTENANCE; } int pendingStops() { return upStops.size() + downStops.size(); } void addStop(int target) { if (target < minFloor || target > maxFloor) throw new IllegalArgumentException("Invalid floor " + target); if (!available()) throw new IllegalStateException("Car " + id + " in maintenance"); if (target == floor && state != CarState.MOVING) { state = CarState.DOORS_OPEN; return; } (target > floor ? upStops : downStops).add(target); if (direction == Direction.IDLE) direction = target > floor ? Direction.UP : Direction.DOWN; } /** Advance the simulation by one tick. */ void step() { if (state == CarState.MAINTENANCE) return; if (state == CarState.DOORS_OPEN) { state = pendingStops() == 0 ? CarState.IDLE : CarState.MOVING; } if (pendingStops() == 0) { direction = Direction.IDLE; state = CarState.IDLE; return; } if (direction == Direction.UP && upStops.isEmpty()) direction = Direction.DOWN; if (direction == Direction.DOWN && downStops.isEmpty()) direction = Direction.UP; state = CarState.MOVING; floor += direction == Direction.UP ? 1 : -1; TreeSet<Integer> stops = direction == Direction.UP ? upStops : downStops; if (stops.remove(floor)) state = CarState.DOORS_OPEN; // stops behind us in the current direction move to the other set (LOOK) if (direction == Direction.UP) { downStops.addAll(upStops.headSet(floor)); upStops.removeIf(f -> f < floor); } else { upStops.addAll(downStops.tailSet(floor, false)); downStops.removeIf(f -> f > floor); } } void setMaintenance(boolean on) { state = on ? CarState.MAINTENANCE : CarState.IDLE; if (on) { upStops.clear(); downStops.clear(); } } @Override public String toString() { return "Car" + id + "@" + floor + " " + direction + " " + state; }} interface DispatchStrategy { Optional<ElevatorCar> choose(List<ElevatorCar> cars, int floor, Direction dir); } final class NearestSuitableCar implements DispatchStrategy { public Optional<ElevatorCar> choose(List<ElevatorCar> cars, int floor, Direction dir) { return cars.stream().filter(ElevatorCar::available).min(Comparator.comparingInt(c -> score(c, floor, dir))); } private int score(ElevatorCar c, int floor, Direction dir) { int distance = Math.abs(c.floor() - floor); boolean onTheWay = c.direction() == Direction.IDLE || (c.direction() == dir && (dir == Direction.UP ? c.floor() <= floor : c.floor() >= floor)); return onTheWay ? distance : distance + 10 * (c.pendingStops() + 1); }} final class ElevatorController { // mediator between buttons and cars private final List<ElevatorCar> cars; private DispatchStrategy strategy; ElevatorController(List<ElevatorCar> cars, DispatchStrategy strategy) { this.cars = List.copyOf(cars); this.strategy = strategy; } void setStrategy(DispatchStrategy s) { strategy = s; } ElevatorCar hallCall(int floor, Direction dir) { ElevatorCar car = strategy.choose(cars, floor, dir).orElseThrow(() -> new IllegalStateException("No car available")); car.addStop(floor); return car; } void carCall(int carId, int floor) { cars.get(carId).addStop(floor); } void tick() { cars.forEach(ElevatorCar::step); }}Complexity and performance
TreeSet insert.
Score each car.
Per car per tick.
Trade-offs
Nearest-car heuristics are easy to explain; optimal scheduling (minimizing total wait) is NP-hard in general, so real systems use heuristics and destination dispatch.
A tick loop is easy to test deterministically; event-driven timing is closer to reality but harder to reason about.
Variants and related techniques
Passengers enter destinations in the lobby; cars are grouped by destination.
Cars serve floor ranges (low-rise, high-rise).
Fire service, VIP, and freight modes as additional states.
Common mistakes
- One giant if/else in a controller for all states.
Fix: Separate car state logic from dispatch strategy.
- Serving requests FIFO.
Fix: Causes excessive back-and-forth; use SCAN/LOOK ordering.
- Ignoring requests for the current floor.
Fix: Open doors immediately when idle at that floor.
Interview questions
How does a car decide where to go next?
It keeps up and down stop sets and uses the LOOK algorithm: continue in the current direction while there are stops ahead, stopping at each; when none remain ahead, reverse; when no stops remain, go idle.
How would you make scheduling configurable?
Inject a DispatchStrategy into the controller (nearest suitable car, least loaded, zoning). The controller acts as a mediator between hall buttons and cars and delegates the choice to the strategy.
How do you handle concurrency?
Hall and car calls can arrive from many threads; synchronize addStop per car or funnel requests through a single controller thread with a queue, while the simulation loop steps cars.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Single-car elevator with LOOK | Medium | Stop ordering. |
| Multi-car system with swappable dispatch | Hard | Strategy + Mediator. |