Free preview

Why this matters: the elevator system is where machine-coding candidates most often build the plausible wrong thing: one big controller object with a queue of requests, served in arrival order. It works in a demo and is wrong in every way an interviewer knows how to press. The correct model is three separations — car from car, assignment from movement, and sweep from queue — each of which you should be able to justify in a sentence.

Start from the events, not the hardware

hallCall(floor, direction)      a rider in the hallway wants a ride
carCall(car, destination)       a rider inside picked a floor
arrived(car, floor)             hardware: the car reached a floor
doorsClosed(car)                hardware: safe to move again

Everything the controller does is a reaction to one of these four. That framing — the controller as a pure event-reactor — is what makes the design testable (inject events, assert commands) and what keeps hardware concerns on the hardware's side of the boundary.

One state machine per car — four instances, not one

Each car is, at any moment, doing exactly one thing: sitting idle, moving up, moving down, or standing with doors open. That's a state machine, and the crucial decision is that each car runs its own instance:

IDLE  --assigned a stop-->        MOVING_UP | MOVING_DOWN
MOVING_x  --arrived at a stop-->  DOORS_OPEN
DOORS_OPEN  --doorsClosed-->      MOVING_x if stops remain, else IDLE

The rejected alternative is one global machine tracking all cars — a state space that multiplies (4 cars × 4 states and their combinations) and couples every car's transitions to every other's. Four independent machines keep each car's behavior locally reasoned: given this car's state and this event, what happens? That's cohesion doing its job — each machine owns one car's motion and nothing else — and it's why the design stays debuggable when car 2 misbehaves at 2 a.m.

The dispatcher: assignment is not movement

Hall calls belong to no car when they're born — someone on floor 7 pressed "down," and choosing which car answers is a decision. Give that decision an owner: a dispatcher that receives every hall call and assigns it to a car. Car calls skip the dispatcher entirely — the rider is already inside a specific car, so the destination goes straight onto that car's plan.

Inside the dispatcher, the how of choosing is a policy, and policies change — so it goes behind a seam:

DispatchStrategy:
    choose(hallCall, cars) -> Car

NearestInDirection implements DispatchStrategy
    // v1: nearest car already heading that way, else nearest idle

This is the same open/closed move as the parking lot's assignment strategy, and the justification is the same shape: how we pick a car will be tuned for the rest of the building's life — the machinery of cars and calls must not change when it is. Lesson 04 exercises this seam hard; here it's enough that assignment logic has exactly one home, and it is not inside a car.

Stops as a directional sweep — not a queue

Here is the mistake the problem exists to catch: storing each car's pending stops as a FIFO queue. It feels natural — requests came in an order, serve them in it — and it produces an elevator that physically ping-pongs. From floor 1, take calls at 3, 18, 4, 17 in arrival order: FIFO visits 3 → 18 → 4 → 17, riding almost the full shaft three times. Any real elevator serves 3, 4 on the way up, then 18… wait, 17 then 18 — the point is it sweeps.

So each car manages stops as a directional sweep (the LOOK algorithm from disk scheduling, in plain clothes): keep moving in the committed direction while stops remain ahead of you; when none remain, reverse or go idle. The clean data shape is two ordered sets per car:

stopsAbove   sorted ascending   — served while MOVING_UP
stopsBelow   sorted descending  — served while MOVING_DOWN
nextStop()   first element of the set matching current direction

A new stop lands in the set that matches its position relative to the car — and the sweep gives the design its second big justification sentence: the queue serves requests in arrival order; the sweep serves them in shaft order, and shaft order is what minimizes total travel.

Invariants, stated out loud

- every accepted hall call is assigned to exactly one in-service car
- a car's stop sets only contain floors consistent with its position
  (stopsAbove strictly above it, stopsBelow strictly below)
- only events mutate car state; commands are outputs, never state
- the dispatcher owns hall-call assignment; cars own their own motion

The first invariant quietly defines the dispatcher's contract (no dropped calls, no double-assignment); the second is what makes nextStop() trivially correct; the last is the separation the whole design rides on.

What we rejected, and why

The god controller. One class holding all cars' positions, all requests, and a tick() that decides everything. It couples every decision to every other, and any change — a new policy, a new car — is surgery in the one class that does everything. The design above is its refutation, piece by piece.

Scheduling inside the Car. Tempting: the car knows where it is, let it grab calls. But then every policy change edits every car, and two cars can't coordinate (both dive for the same call). Assignment is a decision about cars, so it lives above them — the dispatcher. Low coupling between deciding and doing, named at the decision.

One request type. Merging hall and car calls into a generic Request loses the direction information the sweep needs and the unassigned-ness the dispatcher needs. The two types earn their keep.

Key takeaway

The elevator controller is three separations: per-car state machines driven purely by events (four instances, never one global machine), a dispatcher that owns hall-call assignment behind a swappable strategy while car calls go straight to their car, and per-car stops kept as a directional sweep — two ordered sets — because shaft order beats arrival order. State the invariants (one owner per hall call, stop sets consistent with position) and the model is fully defensible before code.

Enjoying the preview?

Create a free account to unlock the rest of this course, the in-browser judge, and live AI mock interviews.

Sign up free to continue