This gives away every stage. For a clean attempt, practise it first, then come back.
The short version
- An elevator system makes two different decisions. Dispatching picks which car answers a button on a floor. Scheduling picks which stop one car serves next. Keep them in separate classes.
- A single car should use LOOK: keep going one way while there’s work ahead, then turn round. First-come-first-served zig-zags; nearest-first can strand a far floor.
- A hall call has a direction. A car going up stops for “up” calls on the way and collects “down” calls on its way back.
- Buttons are pressed from many threads, but the building moves in one place. Buttons leave messages in a queue; one controller thread reads them and moves every car. No locks inside cars.
- Dispatchers are swappable strategies. In our simulation, cost-based dispatch cut average waits from 28 to 10 ticks in the morning rush, but made the worst wait slightly longer. Say both.
How this walkthrough is built
Most LLD problems have one hard idea. The elevator has three: choosing which car answers a call, choosing the order a car serves its stops, and handling requests from dozens of buttons at once. Strong answers keep these three apart. Weak answers mix them into one Elevator class that does everything.
We’ll build it in interview order:
| Step | What you do | What comes out of it |
|---|---|---|
| 1. Understand the problem | Ask questions, write down requirements | Hall calls, car calls, and what “good” means |
| 2. Break it into entities | Nouns, then the two decisions, then the scheduling trap | Separate classes for dispatching and scheduling |
| 3. Choose a direction | Top-down for designing, bottom-up for coding | The order you’ll work in |
| 4. Design each class | Decide what each class knows and does | A card per class |
| 5. Connect the classes | Pick the right relationships | Who owns whom |
| 6. Draw the diagram | Build it up in three versions | The complete class diagram |
| 7. Name the patterns | Strategy, Observer, a command queue | And why the car isn’t a State pattern |
| 8. Walk the flows | A call, a ride, a car that turns round | Three rules you’d otherwise miss |
| 9. Write the code | Bottom-up, one class at a time | Code that compiles and runs |
| 10. Make it safe | Buttons on many threads, one building | A queue and a single controller |
| 11. Extend it | Rush hour, maintenance, fire, capacity | Data from a real simulation |
The one sentence to remember: the building decides which car; each car decides which stop.
Step 1Understand the problem
Picture the real thing first
Think of an office building at 9am. Twenty floors, three lifts. On the ground floor, a crowd presses the up button. On floor 12, someone presses down to go for coffee. Inside lift B, three people have each pressed a different floor number.
There are two kinds of button here, and they’re very different. The buttons on a floor (up and down) say “someone here wants to go in this direction”, but not where. Any lift could answer. The buttons inside a lift say “this lift must stop at floor 14”. Only that lift can answer.
And nobody wants to wait. What “good” means is subtle: short waits on average, but also nobody waiting forever, and lifts not wasting energy rushing back and forth.
Ask before you design
Interviewer: Design an elevator system for a building.
You: Sure. How many floors and how many lifts?
Interviewer: Twenty floors, three lifts. Make the numbers configurable.
You: On each floor, are there up and down buttons, or a keypad where you type your destination?
Interviewer: Up and down buttons. Inside each lift, a button for every floor.
You: Who decides which lift answers a floor button?
Interviewer: The system does. That’s part of what we want to see.
You: What matters most: average wait, longest wait, or energy?
Interviewer: Average wait first, but nobody should be stuck for ages.
You: Do lifts need to handle maintenance?
Interviewer: Yes. A technician can take one out of service.
You: Anything about emergencies?
Interviewer: A fire alarm should send all lifts to the ground floor. Maybe as a follow-up.
You: Weight limits?
Interviewer: Follow-up.
You: Do I control the motors directly, or send commands to a controller?
Interviewer: Assume a simple model: a lift moves one floor per time step, and doors stay open for a couple of steps.
| Question | What the answer decided |
|---|---|
| Floors and lifts, configurable | ElevatorSystem(minFloor, maxFloor, carCount, …) |
| Up/down buttons outside, floor buttons inside | Two kinds of request: hall calls (floor + direction) and car calls (car + floor) |
| The system decides which lift | A dispatcher: a separate decision with its own class |
| Average wait, but nobody stuck | A scheduling rule that’s fair, and we’ll measure both average and worst |
| Maintenance | A car can stop accepting calls, and its calls must move to another car |
| Fire alarm | A special mode that overrides normal work (follow-up) |
| One floor per step | A tick-based model: easy to simulate and test |
Functional requirements
- FR1. Each floor has up and down buttons (only up on the bottom floor, only down on the top). Pressing one creates a hall call.
- FR2. Each car has a button per floor. Pressing one creates a car call for that car.
- FR3. The system assigns every hall call to one car.
- FR4. Each car serves its calls in a sensible order, opening its doors at each stop.
- FR5. A hall call’s light goes off when any car arrives at that floor going that way.
- FR6. A car can be put into maintenance. It finishes the rides of people inside, hands its waiting hall calls to other cars, and then stops.
- FR7. The system can report each car’s floor, direction and state.
Non-functional requirements
- NFR1: Fair. Short average waits, and no call waits forever.
- NFR2: Safe. A car never moves with its doors open, and never skips past a floor in one step.
- NFR3: Correct under concurrency. Buttons are pressed on many threads at once, while cars are moving.
- NFR4: Swappable rules. The dispatching rule can change (rush-hour mode, energy saving) without changing the cars.
- NFR5: Testable. Time is simulated in ticks, so tests can run a whole morning in milliseconds.
Assumptions we’ll write down
| Assumption | Why it’s reasonable |
|---|---|
| A car moves one floor per tick; doors stay open 2 ticks | A simple, deterministic model the interviewer agreed to |
| Pressing a lit button again does nothing | Real buttons work this way |
| No weight limit in the main design | Follow-up |
| All cars serve all floors | Express lifts are a follow-up |
| Cars start at the ground floor | Any start works |
Step 2Break it into entities
Pull out the nouns
Building, floor, lift (car), button, up button, down button, floor button, door, motor, direction, request, call, passenger, technician, controller, display.
| Noun | Own data? | Own rules? | Becomes |
|---|---|---|---|
| Building / controller | all cars, the request queue | which car answers a hall call | ElevatorSystem |
| Lift (car) | floor, direction, state, stops | never move with doors open; serve stops in order | ElevatorCar |
| Direction | — | — | Direction enum (UP, DOWN) |
| Car state | — | — | CarState enum |
| Hall call | floor, direction | — | HallPress record |
| Car call | car, floor | — | CarPress record |
| “Which car answers” | — | a rule that will change | Dispatcher interface |
| Door | open or shut, a timer | — | a state and a counter inside the car |
| Floor | — | — | an int |
| Button | — | — | nothing: pressing it is a method call |
| Passenger, technician | — | — | nothing: outside the system |
Two traps here.
*The Floor class trap.* It’s tempting to create Floor objects holding their buttons. But a floor has no behaviour of its own; it’s just a number that buttons and cars refer to. A class would add objects to create and pass around without adding any rules.
*The Passenger trap. The system never knows how many people are waiting or where they’re going until they’re inside. It sees button presses. (In our simulation, passengers exist, but only in the test*, not in the system.)
The two decisions
Here’s the insight that shapes everything else. An elevator system makes two decisions that look similar but aren’t:
| Dispatching | Scheduling | |
|---|---|---|
| The question | Which car should answer this floor’s button? | Which of my stops do I go to next? |
| Who decides | the building | each car, for itself |
| What it knows | all cars’ positions and loads | its own stops and direction |
| How often | once per hall call | every time the car moves |
| Will it change? | yes: rush hour, energy saving, VIP floors | rarely: LOOK is good almost everywhere |
Mixing them is the most common design mistake on this problem: one class that both picks cars and orders stops. Separating them gives us a Dispatcher (owned by the system, swappable) and scheduling logic inside ElevatorCar.
The scheduling trap: three ways one car can choose
Before classes, settle how one car picks its next stop. Here are three rules on the same seven requests, arriving over time:
First come, first served goes to requests in the order they arrived. Fair in a naive sense, but the car zig-zags across the building and travels much further.
Nearest first always goes to the closest request. Travel drops sharply. But look at the far floor: if new requests keep appearing near the car, a far request can wait a very long time. This is called starvation.
LOOK keeps going in its current direction while there are requests ahead, then turns round. Travel stays low, and nobody waits more than about two sweeps of the building. It’s what real lifts do. (Its cousin SCAN goes all the way to the end of the building before turning; LOOK turns at the last request, which saves the empty trip.)
Choose LOOK for each car, and say why with the trade-off: “Nearest-first has lower average wait in some cases, but it can starve a floor. LOOK bounds the worst case.”
Hall calls have a direction
One more detail that most answers miss. A car going up from floor 2 to 9 passes floor 5. Someone on floor 5 has pressed down. Should the car stop?
No. They want to go down; the car is going up. If it stops, they either get in and ride up first (annoying) or don’t get in (a wasted stop). So the car only stops for hall calls in its own direction, plus all car calls. It collects the “down” call at floor 5 on its way back. The exception: if floor 5 is the last stop in the up direction, the car can stop there, turn round, and answer the down call immediately.
That’s why each car keeps three sets of stops: car calls, up calls and down calls.
Pull out the verbs
| Use case | Who starts it | Steps |
|---|---|---|
| UC1: Call a lift | Someone on a floor | press up/down → dispatcher picks a car → the car adds the call |
| UC2: Choose a floor | Someone inside | press a floor → that car adds a stop |
| UC3: Move | The clock (each tick) | each car: open doors if this floor is a stop, else move one floor by LOOK |
| UC4: Arrive | A car | doors open → hall-call light off → listeners told |
| UC5: Maintenance | Technician | car stops accepting; its hall calls move to other cars; finishes rides; stops |
Give each job to the class that knows the most
| Job | Information needed | Owner |
|---|---|---|
| Which car should take a hall call? | every car’s floor, direction and stops | Dispatcher, given the cars by the system |
| Which stop next? Move or open doors? | this car’s stops, floor, direction | ElevatorCar |
| Is this car on its way to that floor? | its direction and floor | ElevatorCar (answered for the dispatcher) |
| Has this hall call already been assigned? | all assignments | ElevatorSystem |
| Turn a lit button off | which car was assigned | ElevatorSystem |
| Accept presses from any thread | a safe queue | ElevatorSystem |
CRC cards
| Class | Responsibilities | Collaborators |
|---|---|---|
ElevatorSystem | take presses from any thread; each tick, assign hall calls and move cars; track lit buttons; maintenance | ElevatorCar, Dispatcher |
ElevatorCar | hold stops; LOOK; doors; never move with doors open | CarListener |
Dispatcher (+ implementations) | choose a car for a hall call | ElevatorCar |
CarListener | be told when doors open | — |
HallPress, CarPress, Maintenance | describe one request | — |
Find what will change
| Will change | Becomes |
|---|---|
| How cars are chosen (rush hour, energy, VIP) | Dispatcher interface |
| Who reacts to arrivals (floor displays, logs, a phone app) | CarListener interface |
| New kinds of request (fire alarm, VIP key) | new Request records |
| The scheduling inside a car | not an interface: LOOK, unless the interviewer asks |
Step 3Top-down or bottom-up?
Top-down, from the buttons inward: a floor button calls system.pressHallButton(5, DOWN). The system needs to pick a car, so a Dispatcher appears. The dispatcher needs to compare cars, so each car must report its floor, direction and how busy it is. The chosen car needs to remember the call, so assignHallCall appears. To serve it, the car needs a tick() that moves or opens doors.
Bottom-up, for coding: Direction and CarState, then ElevatorCar (the tricky LOOK logic, tested on its own with one car), then dispatchers, then the system that ties them together.
The car is where the subtle bugs live: stopping for the wrong direction, turning too early, moving with doors open. Write it first and test it alone, with a single car, before any dispatching exists.
Step 4Design each class
ElevatorSystem: the building’s controller
Job: accept button presses from anywhere, and on each tick, assign hall calls and move every car.
| Knows | Does | Doesn’t do |
|---|---|---|
| floor range | pressHallButton(floor, dir) from any thread | pick a car (the dispatcher does) |
| all cars | pressCarButton(car, floor) from any thread | order a car’s stops (the car does) |
| the dispatcher | setMaintenance(car, on) | |
| an inbox queue of requests | tick(): the controller thread only | |
| which car each lit hall button is assigned to | ||
| hall calls no car could take yet |
Notice the split between “any thread” methods and tick(). The press methods only validate and enqueue. All the real work happens in tick(), which runs on one controller thread. Step 10 explains why this makes concurrency simple.
ElevatorCar: one lift
Job: serve its own stops in LOOK order, and keep itself safe.
| Knows | Does | Doesn’t do |
|---|---|---|
| id, floor range | addCarStop(floor) | decide which hall calls it gets |
| current floor, direction, state | assignHallCall(floor, dir), cancelHallCall | talk to other cars |
| car stops, up calls, down calls | tick(): open doors here, or move one floor | |
| door timer | startMaintenance(), endMaintenance() | |
| a draining flag (finishing before maintenance) | report: busy(), stopCount(), farthestAhead() |
Why three sets and not one list of stops? Because of the direction rule: the car must know whether a stop at floor 5 is “someone inside wants floor 5” (always stop), “someone at 5 wants up” or “someone at 5 wants down”. Each is a TreeSet, which keeps floors sorted, so “is there any stop above me?” is a quick lookup instead of a scan.
CarState: why not a State pattern here?
After the vending machine, it’s natural to reach for State classes. But compare the two problems. The vending machine has five events, each meaning something different in each of four states. The car has essentially one event, tick(), and its behaviour per state is a few lines. A switch on an enum is clearer here than four classes.
Saying this is a strong signal: patterns are tools for specific problems, not decorations. If the car later gains many events (fire mode, independent service, door-hold button, overload), that’s the moment to move to State classes.
Dispatcher: the swappable rule
Job: given the cars and a hall call, pick a car, or none if no car can take it.
Two implementations:
- *
NearestCarDispatcher* estimates how long each car would take to reach the call, and picks the cheapest. An idle car costs its distance. A car already heading that way, with the floor ahead of it, costs its distance. Any other car must first finish its current run, then come back. Each stop it already has adds a little, because each stop means doors opening and closing. - *
RoundRobinDispatcher* gives calls to cars in turn. It’s a naive baseline, useful for showing that the cost-based rule actually helps.
Requests and listeners
HallPress(floor, dir), CarPress(carId, floor) and Maintenance(carId, on) are small records that travel through the queue. CarListener.doorsOpened(car, floor, leavingDirection) lets floor displays, logs, or a simulation react to arrivals.
Step 5How the classes relate
| Relationship | In this design | Why that kind |
|---|---|---|
| Composition | system owns its cars | cars are created by the system and only make sense inside it |
| Association | system → Dispatcher | given from outside; swappable |
| Realization | NearestCarDispatcher, RoundRobinDispatcher implement Dispatcher | two rules, one contract |
| Realization | HallPress, CarPress, Maintenance implement Request | a closed family of messages |
| Association | car → CarListener | told about arrivals |
| Dependency | dispatcher uses cars passed as a parameter | it holds no references between calls |
Request is a sealed interface: it lists exactly which records can implement it. That lets the system’s switch over requests be checked by the compiler. Add a new request type, and every switch that doesn’t handle it stops compiling, which is exactly what you want.
Step 6The class diagram, built up
Version 1: the building and its cars
Version 2: the two decisions
Version 3: the whole picture
Check it against the requirements
| Requirement | Where it lives |
|---|---|
| FR1: hall buttons, none off the ends | pressHallButton validation; HallPress |
| FR2: car buttons | pressCarButton → ElevatorCar.addCarStop |
| FR3: assign hall calls | ElevatorSystem.assign → Dispatcher.choose |
| FR4: sensible order | ElevatorCar.tick with LOOK and the direction rule |
| FR5: lights off on arrival | assigned map, cleared in opened |
| FR6: maintenance | startMaintenance hands calls back; draining; OUT_OF_SERVICE |
| FR7: report status | floor(), direction(), state() |
| NFR1: fair | LOOK bounds waits; measured in step 11 |
| NFR2: safe | movement only in the IDLE/MOVING branch; one floor per tick; tested |
| NFR3: concurrency | inbox queue + single controller (step 10) |
| NFR4: swappable rules | Dispatcher interface |
| NFR5: testable | ticks, not real time |
Step 7The patterns, and why each one is here
| Pattern | Where | Problem it solved |
|---|---|---|
| Strategy | Dispatcher | the rule for picking a car changes by time of day and building |
| Observer | CarListener | displays, logs and apps react to arrivals without the car knowing them |
| Command queue | Request records in inbox | presses from any thread become messages handled in order by one thread |
| Facade | ElevatorSystem | buttons and panels talk to one object, not to cars and dispatchers |
| Simulation clock | tick() | time is a method call, so tests run instantly and repeat exactly |
The command queue deserves a moment. Each press becomes a small object describing what happened. Instead of every button thread reaching into the cars (and needing locks everywhere), they all drop messages into one queue, and one thread acts on them. This is the same idea as an event loop in a browser or a game. Step 10 shows why it’s the right fit here.
And the pattern deliberately not used: State classes for the car, explained in step 4.
Why the dispatcher doesn’t schedule
A natural question: why not let the dispatcher also tell each car its stop order? Because the car’s order depends on things that change every tick (its floor, new car calls from people inside), and recomputing a global plan every tick is far more complex than “each car runs LOOK on its own stops”. The dispatcher makes one decision per hall call; the car makes one per tick. Each is simple, and they don’t interfere.
Step 8Walk the flows
A hall call, from button to doors
- The button thread only adds a message to the inbox and returns. It never touches a car.
- On the next tick, the controller reads the message. If “6 DOWN” is already lit (assigned), it ignores the duplicate.
- The dispatcher picks a car. The system records the assignment, so the light stays on.
- Over the following ticks, the car moves by LOOK. When it opens its doors at 6, leaving downwards, the system turns the light off and tells the listeners.
One car, a short story
A single car waits at floor 0. Someone on floor 6 presses down, then someone on floor 3 presses up. The car sets off, and after two ticks someone already inside presses 8. Here’s the real run:
t3: doors open at 3 (UP)
t11: doors open at 8 (UP)
t16: doors open at 6 (DOWN)The car passed floor 6 on its way up without stopping, because that call was for going down. It went on to 8 (the car call), found nothing further up, turned round, and collected the down call at 6 on the way back. That’s LOOK plus the direction rule, working together.
Passing a call, then coming back for it
One car at 0, going to 9. Calls: floor 4 going down, floor 6 going up.
stops in order: [0U, 6U, 9U, 4D]Up from 0: stops at 6 (up call, same direction), skips 4 (down call), reaches 9, turns, and stops at 4 on the way down.
How the dispatcher chose
Two cars: car 0 idle at floor 0, car 1 at floor 6 going up to 12. The real cost numbers:
car0 at 0, car1 at 6 going UP: call 8 UP costs car0=8, car1=4; call 2 UP costs car0=2, car1=18A call at 8 going up is on car 1’s way: 2 floors, plus 2 for its one existing stop, so 4. Car 0 would need 8. Car 1 wins. A call at 2 going up is behind car 1, which would have to go up to 12 and come back: 6 + 10 + 2 = 18. Car 0, idle and two floors away, wins easily.
What walking the flows taught us
| Found while walking | Consequence |
|---|---|
| A car going up shouldn’t stop for a down call | stops are split by direction; the turnaround floor is the exception |
| Two cars might both reach a lit floor | on arrival, the light goes off and the other car’s copy of the call is cancelled |
| A duplicate press must not assign a second car | the system remembers which hall calls are lit |
| Nobody may be waiting when a car goes into maintenance | its hall calls are handed back and reassigned |
| No car may be able to take a call (all in maintenance) | the call waits in a list and is retried each tick |
The life of a car
| State | On tick | Next |
|---|---|---|
| Idle | no stops: stay. Stops: serve here, or start moving | Idle, Doors open or Moving |
| Moving | stop here? open doors. Otherwise move one floor | Moving or Doors open |
| Doors open | count down; when done, move on or go idle | Doors open, Moving, Idle, or Out of service if draining |
| Out of service | nothing | Idle when maintenance ends |
Moving happens in exactly one place, the Idle/Moving branch. There’s no code path that changes the floor while the doors are open. That’s NFR2, guaranteed by the shape of the code rather than by a check that could be forgotten.
When things go wrong
| Situation | What happens |
|---|---|
| “Up” pressed on the top floor | rejected straight away |
| A car button for a floor that doesn’t exist | rejected |
| The same hall button pressed 50 times | assigned once; the rest are ignored |
| Every car is in maintenance | the call waits and is assigned when a car returns |
| A car is taken out of service with calls | its hall calls move to other cars; people inside are still delivered |
Step 9Write the code, bottom-up
All of this compiles and runs; every output on this page comes from it.
9.1 Directions, states and listeners
enum Direction {
UP, DOWN;
Direction opposite() { return this == UP ? DOWN : UP; }
}
enum CarState { IDLE, MOVING, DOORS_OPEN, OUT_OF_SERVICE }
interface CarListener { void doorsOpened(ElevatorCar car, int floor, Direction leaving); }9.2 ElevatorCar: fields and requests
final class ElevatorCar {
static final int DOOR_TICKS = 2;
private final int id, minFloor, maxFloor;
private final CarListener listener;
private final TreeSet carStops = new TreeSet<>(); 1
private final TreeSet upCalls = new TreeSet<>(); 2
private final TreeSet downCalls = new TreeSet<>();
private int floor;
private Direction dir = Direction.UP;
private CarState state = CarState.IDLE;
private int doorTicks;
private boolean draining;
private int travelled;
ElevatorCar(int id, int minFloor, int maxFloor, int startFloor,
CarListener listener) {
this.id = id; this.minFloor = minFloor; this.maxFloor = maxFloor;
this.floor = startFloor; this.listener = listener;
}
void addCarStop(int f) {
if (f < minFloor || f > maxFloor)
throw new IllegalArgumentException("no floor " + f);
if (state == CarState.OUT_OF_SERVICE) return;
if (f == floor && state == CarState.DOORS_OPEN) return; 3
carStops.add(f);
}
private TreeSet calls(Direction d) {
return d == Direction.UP ? upCalls : downCalls;
}
void assignHallCall(int f, Direction d) { calls(d).add(f); }
void cancelHallCall(int f, Direction d) { calls(d).remove(f); }
boolean accepting() { return state != CarState.OUT_OF_SERVICE && !draining; }
boolean busy() {
return !carStops.isEmpty() || !upCalls.isEmpty() || !downCalls.isEmpty();
}
int stopCount() { return carStops.size() + upCalls.size() + downCalls.size(); } - 1pressed inside
- 2floor buttons, going up
- 3already here, doors open
9.3 ElevatorCar: one tick
void tick() {
switch (state) {
case OUT_OF_SERVICE -> { }
case DOORS_OPEN -> {
if (--doorTicks > 0) return; 1
state = busy() ? CarState.MOVING
: draining ? CarState.OUT_OF_SERVICE : CarState.IDLE;
}
case IDLE, MOVING -> {
if (!busy()) {
state = draining ? CarState.OUT_OF_SERVICE : CarState.IDLE;
return;
}
if (serveHere()) return; 2
if (!anyAhead(dir)) dir = dir.opposite(); 3
floor += dir == Direction.UP ? 1 : -1; 4
travelled++;
state = CarState.MOVING;
}
}
}- 1still open
- 2open doors instead of moving
- 3LOOK: turn at the last stop
- 4the only line that moves
This method is the whole safety story. The floor changes on exactly one line, note (4), and that line is only reachable when the state is Idle or Moving, never Doors open. And it changes by exactly one.
9.4 ElevatorCar: should I stop here?
private boolean serveHere() {
boolean turning = !anyAhead(dir); 1
boolean stop = carStops.contains(floor)
|| calls(dir).contains(floor)
|| (turning && (upCalls.contains(floor) || downCalls.contains(floor)));
if (!stop) return false;
carStops.remove(floor);
boolean sameWay = calls(dir).remove(floor);
if (!sameWay && turning && calls(dir.opposite()).remove(floor)) {
dir = dir.opposite(); 2
}
state = CarState.DOORS_OPEN;
doorTicks = DOOR_TICKS;
listener.doorsOpened(this, floor, dir);
return true;
}
private boolean anyAhead(Direction d) {
for (TreeSet s : List.of(carStops, upCalls, downCalls)) {
Integer next = d == Direction.UP ? s.higher(floor) : s.lower(floor); // (3)
if (next != null) return true;
}
return false;
} - 1nothing further this way
- 2answer the other button
Read the stop condition as three plain rules: stop for anyone inside who wants this floor; stop for people here going my way; and if this is as far as I go, stop for people here going either way. Note (2) handles the turnaround: if we stopped for a call in the other direction, the car now leaves in that direction, and the listener is told which way it’s leaving, so the right people get in.
Note (3) is why the stops are TreeSets: higher(floor) finds the next stop above in O(log n), without scanning.
9.5 ElevatorCar: maintenance and the dispatcher’s questions
// Hand back hall calls so another car can take them; finish car stops, then stop.
List startMaintenance() {
draining = true;
List back = new ArrayList<>();
for (int f : upCalls) back.add(new HallPress(f, Direction.UP));
for (int f : downCalls) back.add(new HallPress(f, Direction.DOWN));
upCalls.clear();
downCalls.clear();
return back;
}
void endMaintenance() {
draining = false;
if (state == CarState.OUT_OF_SERVICE) state = CarState.IDLE;
}
// the farthest stop in the current direction, or the current floor if none
int farthestAhead() {
Integer far = null;
for (TreeSet s : List.of(carStops, upCalls, downCalls)) {
if (s.isEmpty()) continue;
int f = dir == Direction.UP ? s.last() : s.first();
if (far == null || (dir == Direction.UP ? f > far : f < far)) far = f;
}
return far == null ? floor : far;
}
int id() { return id; }
int floor() { return floor; }
Direction direction() { return dir; }
CarState state() { return state; }
int travelled() { return travelled; }
} Maintenance doesn’t stop the car instantly. People inside still get to their floors (the car stops are kept); only the hall calls, which another car can answer, are handed back. When the car runs out of work, it goes out of service by itself.
9.6 The dispatchers
interface Dispatcher {
Optional choose(List cars, int floor, Direction d);
}
final class NearestCarDispatcher implements Dispatcher {
public Optional choose(List cars, int floor, Direction d) {
return cars.stream()
.filter(ElevatorCar::accepting)
.min(Comparator.comparingInt(c -> cost(c, floor, d)));
}
static int cost(ElevatorCar c, int floor, Direction d) {
int here = c.floor();
if (!c.busy()) return Math.abs(here - floor); 1
boolean onTheWay = c.direction() == d
&& (d == Direction.UP ? floor >= here : floor <= here);
int far = c.farthestAhead();
int distance = onTheWay ? Math.abs(floor - here) 2
: Math.abs(far - here) + Math.abs(far - floor); 3
return distance + 2 * c.stopCount(); 4
}
}
final class RoundRobinDispatcher implements Dispatcher {
private int next;
public Optional choose(List cars, int floor, Direction d) {
for (int i = 0; i < cars.size(); i++) {
ElevatorCar c = cars.get((next + i) % cars.size());
if (c.accepting()) {
next = (next + i + 1) % cars.size();
return Optional.of(c);
}
}
return Optional.empty();
}
} - 1idle: just the distance
- 2heading there already
- 3finish the run, come back
- 4each stop costs door time
The cost is an estimate, not an exact prediction. Exact arrival times would depend on future presses nobody knows about yet. A simple, explainable estimate is what you want in an interview, and as step 11 shows, it already makes a large difference.
9.7 Requests
sealed interface Request permits HallPress, CarPress, Maintenance { }
record HallPress(int floor, Direction dir) implements Request { }
record CarPress(int carId, int floor) implements Request { }
record Maintenance(int carId, boolean on) implements Request { }9.8 ElevatorSystem
final class ElevatorSystem {
private final int minFloor, maxFloor;
private final List cars = new ArrayList<>();
private final Dispatcher dispatcher;
private final Queue inbox = new ConcurrentLinkedQueue<>(); 1
private final Map assigned = new HashMap<>(); 2
private final Deque waiting = new ArrayDeque<>(); 3
private final List listeners = new CopyOnWriteArrayList<>();
private long now;
ElevatorSystem(int minFloor, int maxFloor, int carCount, Dispatcher dispatcher) {
this.minFloor = minFloor; this.maxFloor = maxFloor; this.dispatcher = dispatcher;
for (int i = 0; i < carCount; i++)
cars.add(new ElevatorCar(i, minFloor, maxFloor, minFloor, this::opened));
}
// any thread: buttons just leave a message
public void pressHallButton(int floor, Direction d) {
if (floor < minFloor || floor > maxFloor)
throw new IllegalArgumentException("no floor " + floor);
boolean offTheEnd = (floor == maxFloor && d == Direction.UP)
|| (floor == minFloor && d == Direction.DOWN);
if (offTheEnd)
throw new IllegalArgumentException("no " + d + " button on floor " + floor);
inbox.add(new HallPress(floor, d));
}
public void pressCarButton(int carId, int floor) {
inbox.add(new CarPress(carId, floor));
}
public void setMaintenance(int carId, boolean on) {
inbox.add(new Maintenance(carId, on));
}
public void addListener(CarListener l) { listeners.add(l); }
// the controller thread only: one step of the whole building
public void tick() {
Request r;
while ((r = inbox.poll()) != null) handle(r);
for (int i = waiting.size(); i > 0; i--) assign(waiting.poll()); 4
for (ElevatorCar c : cars) c.tick();
now++;
}
private void handle(Request r) {
switch (r) { 5
case HallPress h -> assign(h);
case CarPress c -> cars.get(c.carId()).addCarStop(c.floor());
case Maintenance m -> {
ElevatorCar car = cars.get(m.carId());
if (!m.on()) { car.endMaintenance(); return; }
for (HallPress h : car.startMaintenance()) {
assigned.remove(key(h.floor(), h.dir()));
assign(h);
}
}
}
}
private void assign(HallPress h) {
if (assigned.containsKey(key(h.floor(), h.dir()))) return; 6
Optional car = dispatcher.choose(cars, h.floor(), h.dir());
if (car.isEmpty()) { waiting.add(h); return; }
car.get().assignHallCall(h.floor(), h.dir());
assigned.put(key(h.floor(), h.dir()), car.get());
}
private void opened(ElevatorCar car, int floor, Direction leaving) {
ElevatorCar owner = assigned.remove(key(floor, leaving)); 7
if (owner != null && owner != car) owner.cancelHallCall(floor, leaving);
for (CarListener l : listeners) l.doorsOpened(car, floor, leaving);
}
private static String key(int floor, Direction d) { return floor + d.name(); }
List cars() { return cars; }
long now() { return now; }
} - 1the only shared thing
- 2lit: “5UP” → car
- 3no car free yet
- 4retry stuck calls
- 5sealed: all cases
- 6already lit
- 7the light goes off
Note (7) handles a case the flows revealed: car 1 was assigned “5 UP”, but car 0 happened to stop at floor 5 going up first (for someone inside). The people at 5 get into car 0, the light goes off, and car 1’s now-pointless stop is cancelled.
9.9 Maintenance in action
Two cars. A down call at floor 5 is assigned to car 0, and then car 0 is taken out of service:
maintenance: [car 1 at 5], car0 state OUT_OF_SERVICE
top floor: no UP button on floor 9Car 0 handed its call back, the dispatcher gave it to car 1, and car 1 answered it. Car 0, with nobody inside, went out of service.
Step 10Make it safe when buttons are pressed everywhere
Interviewer: Buttons are pressed on many threads, while the lifts are moving. How do you keep this safe?
You: There are two common designs. Let me compare them.
Option A: a lock everywhere
Give each car a lock and have button threads call into cars directly: car.addCarStop() takes the car’s lock. The dispatcher reads every car’s floor and stops, so it must lock all cars at once (always in the same order, to avoid deadlocks). The tick thread locks each car while moving it.
It can be made correct, but every method needs thought about who else might be running, and the dispatcher’s “lock all cars” step is the kind of code that hides deadlocks.
Option B: a queue and one controller thread
Button threads never touch cars. They validate the press and drop a message into a ConcurrentLinkedQueue, a queue built for many threads adding at once. One controller thread runs tick(): it empties the queue, assigns calls, and moves every car. Since only that thread ever touches cars, assignments and the lit-button map, none of them need locks at all.
| Locks everywhere | Queue + controller | |
|---|---|---|
| Locks in cars | yes, every method | none |
| Deadlock risk | real (dispatcher locks many cars) | none |
| Order of events | depends on thread timing | exactly the order they were read |
| Testing | needs threads | call tick() in a loop |
| Cost | — | a press waits up to one tick before it’s acted on |
Option B is the better fit, because the building physically moves in steps anyway; nobody notices a fraction of a tick’s delay before a button is handled. It’s the same design games and user interfaces use: many inputs, one loop that updates the world.
Prove it
Eight threads press 16,000 random hall and car buttons as fast as they can, while the controller ticks. After every tick, a checker compares each car’s floor before and after: did any car move while its doors were open, or jump more than one floor? At the end: was every lit button answered?
concurrent: 16000 presses, every lit button answered=true,
moved with doors open or skipped a floor=false, all idle=trueStep 11Follow-ups, with real numbers
Does the dispatcher actually help?
Interviewer: Is your cost-based dispatcher better than just taking turns? How do you know?
Don’t guess: simulate. We ran 300 passengers through a 20-floor building with three cars, one arriving every two ticks. In the morning rush, 80% start at the ground floor going up. In mixed traffic, 20% do and the rest travel between random floors. The same passengers, in the same order, for both dispatchers:
| Traffic | Dispatcher | Average wait | Longest wait | Floors travelled |
|---|---|---|---|---|
| Morning rush | Nearest car | 10 ticks | 104 | 791 |
| Morning rush | Round robin | 28 | 94 | 926 |
| Mixed | Nearest car | 21 | 93 | 803 |
| Mixed | Round robin | 31 | 92 | 954 |
Cost-based dispatch cut the average wait by almost two-thirds in the rush and by a third in mixed traffic, while travelling 15% less. But look at the longest wait column: it’s slightly worse. Choosing the cheapest car for every call is greedy; occasionally one unlucky call keeps losing to cheaper ones. That’s the same trade-off as nearest-first scheduling, one level up.
Saying this out loud is far stronger than claiming the dispatcher is simply better. The fix, if the interviewer cares about the worst case, is to add the call’s age to the cost, so a call that’s been waiting gets an ever stronger claim on the next car.
Morning rush mode
Interviewer: At 9am everyone starts at the lobby. Can you do better?
A new Dispatcher: between 8:30 and 10:00, send any idle car straight back to the ground floor instead of waiting where it stopped. Some buildings also split floors into zones (car A serves 1–7, car B serves 8–14), which cuts the number of stops per trip. Both are new strategy classes; the cars don’t change. A TimeOfDayDispatcher can hold the others and pick one by the clock. That’s a strategy that chooses strategies, and it’s still just a Dispatcher.
Fire alarm
Interviewer: When the fire alarm sounds, all lifts must go to the ground floor, open their doors, and stay there.
This is a new mode that overrides everything, so it deserves its own request and a new car state:
- A
FireAlarmrecord joins the sealedRequestfamily. The compiler then points at everyswitchthat must handle it. - On
FireAlarm, the system clears all hall calls and lit buttons, and tells every carrecall(groundFloor). - The car drops all stops, sets a single stop at the ground floor, and enters a new
FIRE_RECALLstate. In that state it ignores new car calls, and when it arrives it opens its doors and stays.
Now the car has several events with different meanings per state: tick, car calls, recall, maintenance. This is the moment, mentioned in step 4, when moving the car to the State pattern starts to pay off. Say so.
Weight limits
Interviewer: A lift can carry 8 people.
The car gets a load sensor reading. When full, it doesn’t stop for hall calls (car calls still count, so people can get out), and the dispatcher adds a large cost for full cars. If a full car passes a lit floor, the system must make sure another car still has that call: on arrival, only turn the light off if the doors actually opened for it. Our opened method already ties “light off” to “doors opened leaving that way”, so the change is small.
Destination dispatch
Interviewer: The new building has keypads in the lobby: you type your floor, and the screen tells you which lift to take.
Now hall calls carry a destination, not just a direction. The dispatcher can group people going to nearby floors into the same car, which is why these systems move more people per hour. The model changes: HallPress(floor, dir) becomes DestinationRequest(from, to), the dispatcher answers immediately (so the screen can say “Lift B”), and the car gets both stops at once. The car’s LOOK logic doesn’t change at all, which shows the scheduling and dispatching split was the right cut.
Saving energy
Interviewer: At night the building is nearly empty. Can lifts save power?
A night-mode dispatcher prefers cars that are already moving over idle ones (a moving car costs less extra energy than starting a parked one), and lets idle cars park and sleep. Another strategy class. If the interviewer pushes on how much energy, the honest answer is that it depends on the motors, and you’d measure it with the same kind of simulation as above, adding a cost per start and per floor.
Mistakes people make on this one
- *One
Elevatorclass that dispatches, schedules and moves,* with no separation of the two decisions. - First-come-first-served scheduling, or nearest-first without mentioning starvation.
- Ignoring hall-call direction, so cars stop for people going the other way.
- *A
Floorclass and aPassengerclass* that hold no rules. - Button threads calling into cars directly with no plan for locking.
- A dispatcher that locks every car in whatever order it finds them.
- Moving the car in more than one place, so a doors-open check can be forgotten.
- Maintenance that strands waiting people because their hall calls stay with the broken car.
- Claiming an algorithm is “optimal” without saying optimal for what.
How this answer gets scored
| Moment | What earns credit | What costs you |
|---|---|---|
| Requirements | Hall vs car calls; asking what “good” means | Treating all buttons the same |
| Structure | Dispatching and scheduling as separate decisions and classes | One class doing everything |
| Scheduling | LOOK, with the trade-off against FCFS and nearest-first | “Go to the nearest request” with no caveat |
| Direction | Stopping only for same-direction hall calls; the turnaround case | Stopping for everyone |
| Patterns | Dispatcher as Strategy; listeners; requests as messages; why not State (yet) | State classes for a one-event machine |
| Safety | Movement in exactly one place; never with doors open | A check sprinkled in several places |
| Concurrency | A queue and one controller, with the trade-off against locks | “Make the methods synchronized” |
| Follow-ups | Measured comparisons, honest about worst cases; new strategies and requests | Claims without numbers; editing cars for every feature |
The whole design on one page
- Two decisions: the system’s
Dispatcherpicks a car per hall call; eachElevatorCarpicks its next stop. - Scheduling: LOOK. Keep going while there’s work ahead, then turn round.
- Direction: three sets per car (car stops, up calls, down calls). Stop for car stops and same-direction calls; at the turnaround, either direction.
- Safety: the floor changes on one line, only when doors are shut, by one floor.
- Concurrency: buttons enqueue
Requestmessages; one controller thread runstick(). No locks in cars. - Dispatching: a cost estimate (distance, on-the-way, existing stops). Measured: average wait from 28 to 10 in the rush, worst wait slightly worse. Add call age to fix that.
- Follow-ups: new dispatchers (rush hour, energy), new requests (fire), new states when the car gains enough events.
Stay in the loop
Get new problems and walkthroughs by email
One short email when something new is published: a deep-dive walkthrough, an LLD problem, or a real outage retold. No spam, unsubscribe any time.