Design an Elevator System

Design an elevator LLD: hall calls, car calls, a SCAN state machine, and a controller that assigns each request to exactly one car.

Page content

Alice is on floor 8 and presses Down. Car A is idle on 9, doors closed, going nowhere. Car B is on 2, moving up, with Bob inside and a stop at 5.

If both cars claim her, two cabins arrive for one person — or neither commits and she waits. If Car B reverses at 3 to “help,” Bob never reaches 5 and the cabin yo-yos. This is an LLD interview: objects, a state machine, and one assignment — not elevator-as-a-service.

The main design question is:

How do we accept hall calls and car calls, move one or more cars, and never serve a request twice or reverse unnecessarily?

We will start with one car that re-picks the closest pending floor every tick. SCAN, a controller, and a uniqueness set appear when that car starts chasing the newest button. Same habit as Design a Parking Lot: name the invariant before the class list. Many cars means many Elevator objects and one Controller. If they later say “a tower with eight banks,” each bank is this design.

1. Clarify the problem

“Design an elevator” can mean a coding puzzle with one cabin or a bank that must not strand people. The questions that change the objects:

  • How many floors? How many cars?
  • Hall panels: Up and Down, except the top and bottom?
  • Inside the car: one button per floor?
  • Capacity or weight limit?
  • Do we model door time, or teleport between floors?
  • Emergency / fire service? (Usually skip.)
  • Destination dispatch (pick a floor in the lobby), or classic Up/Down?

If the interviewer gives no extra constraints, I would state:

Floors              10, numbered 1–10
Cars                2 (then generalize to N)
Hall buttons        UP and DOWN; floor 1 UP only, floor 10 DOWN only
Car buttons         One per floor, per car
Capacity            Count of passengers; full cars skip hall stops
Doors               Open, dwell, close on the same tick loop
Emergency           Out of scope (mention and skip)
Dispatch            Classic hall call + car call, not destination panels
Time                Discrete step(): one floor or one door action per tick

Express cars, destination dispatch, and fire mode sit on the same machines. They do not change “one hall call, one assignee.” Say so and move on.

2. Functional requirements

The system must:

  1. Accept a hall call (floor, direction) from the landing.
  2. Accept a car call (elevator, floor) from inside a cabin.
  3. Move cars, open and close doors, and stop at the right floors.
  4. Assign each pending hall call to at most one car.
  5. Keep traveling in the current direction until that sweep is empty.
  6. Report where each car is and what is still pending.

Not in v1: destination keypads in the lobby, express / local banks, VIP override, voice control. Those reuse HallCall, CarCall, and step(). They do not add a message bus.

3. Non-functional requirements

This is not a 50k QPS problem. Interviewers want a state machine and a scheduler, not a cluster.

RequirementTarget
SafetyA car does not reverse with people who boarded for the old direction
Uniqueness(8, DOWN) exists once; two fingers share it
AssignmentOne car owns a hall call, or none yet — never two
LatencySeconds per floor; correctness over a clever heuristic
ConcurrencyButtons and step() share one lock on the pending sets
ClockThe simulator tick, not wall-clock threads per floor

A missed hall call is a product bug. Two cars arriving for one Down press is the same class of bug as two parking tickets for one stall.

Edge cases

  • Alice and Bob both press Down on 8.
  • The assigned car is full when it reaches 8.
  • Someone holds the door; a bag blocks the close sensor.
  • Power is cut while Car B is between 4 and 5.
  • A hall call for the floor the car is already standing on.
  • Up and Down both pending on the same floor.
  • Every car is in MAINTENANCE.
  • A car call for the floor the passenger is already on.

4. Scale

Say the numbers so nobody spends the interview on QPS.

Floors                10 – 50 in one bank
Cars                  1 – 8 in one bank
Hall + car presses    a few per minute at rush hour
Ticks                 one floor-move or door action, ~1–2 seconds of real time

A scan of eight cars and a few dozen pending floors is free. The hard part is who owns Alice and whether Car B may turn around.

If they push “a 90-story tower,” do not invent Redis. Split the building into banks (low-rise, mid-rise, high-rise). Each bank is this controller. Cars do not hop banks.

Tower
  ├── Bank L  floors 1–20     ← this design
  ├── Bank M  floors 1, 20–40 ← this design
  └── Bank H  floors 1, 40–90 ← this design

5. Entities / what we store

Six facts. The controller is the assigner; the car is the mover.

Floor          number; knows which hall buttons exist
HallCall       (floor, direction)          UP | DOWN
CarCall        (elevator_id, floor)
Door           CLOSED | OPENING | OPEN | CLOSING
Elevator       id, floor, state, direction, carCalls, load, door
Controller     elevators[], pendingHall: Set<HallCall>, assignment map

A hall call is “someone on 8 wants to go down.” It is not Alice’s name. A car call is “this cabin must visit 5” because Bob pressed 5 inside. The pending hall set plus each car’s car-call set are the source of truth. The assignment map ((8, DOWN) → A) is derived from that set: it names who is allowed to clear the call.

Hall (8, DOWN) pending, unassigned
  controller assigns A
Hall (8, DOWN) pending, owner A
  A opens on 8 going DOWN
Hall (8, DOWN) gone
  Alice’s destination is a CarCall on A, not a second hall call

Do not store a list of people at the landing. Two Down presses on 8 are one HallCall. Passenger identity is out of scope unless they ask for weight.

6. APIs or class methods

In LLD you may show a service, not HTTP. Either is fine.

requestHall(floor, direction) -> accepted
requestCar(elevator_id, floor) -> accepted
status() -> snapshot
step() -> void          // the clock

Hall call

Request:

POST /v1/buildings/b_tower/hall-calls
{ "floor": 8, "direction": "DOWN" }

Response (new):

HTTP 202
{ "floor": 8, "direction": "DOWN", "assigned_elevator": "A" }

Response (already pending — Alice then Bob on the same panel):

HTTP 200
{ "floor": 8, "direction": "DOWN", "assigned_elevator": "A", "deduped": true }

A second Down on 8 does not create a second job and does not reassign. The lamp is already lit.

Invalid: Down on floor 1, Up on floor 10, unknown floor. Those are 400.

Car call

Request:

POST /v1/buildings/b_tower/elevators/B/car-calls
{ "floor": 5 }

Response:

HTTP 202
{ "elevator_id": "B", "floor": 5, "car_calls": [5] }

Pressing 5 twice in Car B is a no-op. A car call for a floor the car is sitting on with doors closed becomes “open now,” not a trip around the building.

Status

Request:

GET /v1/buildings/b_tower/status

Response:

HTTP 200
{ "elevators": [
    { "id": "A", "floor": 9, "state": "IDLE", "door": "CLOSED", "car_calls": [], "load": 0 },
    { "id": "B", "floor": 2, "state": "MOVING_UP", "door": "CLOSED", "car_calls": [5], "load": 1 }
  ],
  "pending_hall": [{ "floor": 8, "direction": "DOWN", "assigned": "A" }] }

The lobby display can read this. Assignment still goes through requestHall(), not through a guest staring at the arrows.

step() is not an HTTP API. It is the simulator the interview wants: one shared clock, no Thread per floor.

7. Start with one car that re-scans every tick — show the yo-yo

The first design everyone writes:

function step(car):
  targets = all pending hall floors + car.carCalls
  if targets is empty:
    car.state = IDLE
    return
  next = closest(car.floor, targets)     // SSTF
  move one floor toward next
  if car.floor == next:
    open doors, clear that request

It is easy to draw and easy to break.

t=0   Car at 5. Alice presses 8 DOWN. Target 8. Car goes up.
t=1   Floor 6. Bob presses 2 UP. Closest is now 2. Car reverses.
t=2   Floor 5. Charlie presses 9 DOWN. Closest is 9. Car reverses.
t=3   Alice still on 8. Bob still on 2. The cabin is a yo-yo.

The bug is not “we used a loop.” The bug is re-deciding the destination every tick from a global pile of floors, with no committed direction. Closest-first (SSTF, shortest seek) starves far landings and turns the car around the moment a nearer button lights up.

FCFS is the other naive pile: a queue of requests in arrival order.

queue:  (8, DOWN Alice),  (2, UP Bob),  (10, DOWN)
serve Alice, then ride empty to 2, then empty to 10

No starvation, but the car crosses the building once per person. Fine for a freight lift. Wrong for a lobby at 9 a.m.

SSTF / closest each tick     yo-yos; far floors starve
FCFS queue                   no starve; terrible travel
SCAN / LOOK                  commit a direction, then reverse

We will use LOOK (the elevator algorithm). People still say SCAN. Name both; implement LOOK.

8. State machine

One machine per car. Do not hide motion in if floor < dest soup.

                 request / assign
                   ┌──────┐
          ┌───────►│ IDLE │◄──────────────────┐
          │        └──┬───┘                   │
          │           │ start sweep           │ sweep empty
          │           ▼                       │
          │     MOVING_UP ◄──reverse──► MOVING_DOWN
          │           │                       │
          │           │ stop at floor         │
          │           ▼                       │
          │      DOOR_OPEN ── dwell ──► close, continue
          │  fault / power / inspect
          └──────────► MAINTENANCE ── restore ──► IDLE

IDLE may start a sweep or open in place. MOVING_* may open, reverse when the sweep is empty, idle, or enter MAINTENANCE. DOOR_OPEN is a state of the car, not a second scheduler. The door object is the sub-state (OPENING / OPEN / CLOSING / CLOSED) so a blocked sensor can reopen without inventing ALICE_IS_BOARDING.

CLOSED ── arrive ──► OPENING ──► OPEN ── dwell ──► CLOSING ──► CLOSED
                       ▲                              │
                       └──── obstruction / hold ──────┘

Rules that keep Bob safe:

  • A car in MOVING_UP does not become MOVING_DOWN until the up sweep is empty (no car calls above, no assigned up hall calls ahead).
  • Opening on 8 going down clears (8, DOWN) and any car call for 8. It does not clear (8, UP).
  • MAINTENANCE drops this car’s assignments back to the controller. It does not delete hall calls.

Illegal: IDLE with a non-empty committed sweep, or two cars listed as owner of (8, DOWN).

Capacity is not a state. It is a flag the stop rule reads: if load >= capacity, skip hall stops; still honor car calls so people can get off.

9. SCAN / LOOK — commit a direction

Disk SCAN travels to the end of the platter, then reverses. An elevator that rides empty to floor 10 because “SCAN goes to the end” is a waste of a tick. LOOK reverses at the last request in this direction. That is the elevator algorithm.

Car B, MOVING_UP, floor 2
  carCalls = {5}
  assigned hall = none

Sweep UP: stop at 5, open, Bob leaves or stays
No more targets ≥ 5 → reverse or idle

Collect every stop that is ahead and compatible, then move.

stopsAhead(car):
  if car.direction == UP:
    carCalls where floor > car.floor
    + assigned hall calls where dir == UP and floor > car.floor
  if car.direction == DOWN:
    carCalls where floor < car.floor
    + assigned hall calls where dir == DOWN and floor < car.floor

On arrival at floor:

open doors
remove carCall(this, floor)
if direction == DOWN: remove HallCall(floor, DOWN)   // only if assigned to us
if direction == UP:   remove HallCall(floor, UP)

If Alice boarded going down and presses 3, that is requestCar(A, 3), added to the current down sweep. She does not become a new hall call.

FCFS orders people. LOOK orders direction. Car idle on 10, all three already pending:

Alice 8 DOWN, Charlie 2 UP, Bob 9 DOWN

FCFS:  10 → 8 (Alice) → 2 (Charlie) → 9 (Bob)
LOOK:  10 → 9 (Bob) → 8 (Alice) → reverse → 2 (Charlie)
       (2, UP) is not a DOWN stop; do not open for Charlie on the way down

Once you are going down, you take every down stop you already own. You do not ride to Charlie until that sweep is empty.

Same-floor, opposite direction is two visits:

Floor 8: (8, DOWN) and (8, UP) both pending
A car going DOWN opens, Alice boards, (8, DOWN) dies
(8, UP) stays lit until a car arrives going UP

That is why direction lives on the hall call, not only on the floor number.

10. Two cars — the controller assigns once

One car does not need an assigner. Two cars do. The controller is the only object allowed to write assignment[(floor, dir)].

Default policy: direction-compatible, least extra travel. Idle cars compete on distance. A busy car only receives a hall call it can serve without reversing.

cost(car, HallCall(f, d)):

  MAINTENANCE or out of service          → ∞
  full and this would be a hall pickup   → ∞

  IDLE:
    |car.floor - f|                      // nearest idle

  MOVING_UP:
    if d == UP and f >= car.floor:       f - car.floor
    else:                                ∞

  MOVING_DOWN:
    if d == DOWN and f <= car.floor:     car.floor - f
    else:                                ∞

  DOOR_OPEN:
    treat as the direction it will resume,
    or as IDLE if the sweep is about to empty

Pick argmin cost. Tie: lower car id, or the car that is already empty. Say the tie-break.

Look-ahead (optional): a car committed UP may be assigned a DOWN call it will serve after it reverses. Cost is “finish the up sweep, then travel to f.” I would not default to that. It is how Car B steals Alice while Bob is still going to 5. Never assign (8, DOWN) to a car that is MOVING_UP and already at floor > 8 — that is an immediate reverse. Look-ahead for a car still below 8 means “extend this up trip to 8, then turn.” Say it if you use it. Default: Car B does not get Alice.

Alice, Car A, Car B

  9  Car A  IDLE
  8  Alice  presses DOWN
  5  Bob's car call
  2  Car B  MOVING_UP
cost(A, (8, DOWN)) = |9 - 8| = 1
cost(B, (8, DOWN)) = ∞          // committed UP, default policy
assign A

Car A goes IDLE → MOVING_DOWN, one floor, DOOR_OPEN on 8. Car B continues 2 → 3 → 4 → 5, opens for Bob. Nobody reversed.

If the assigner is sloppy and both cars take the call:

A starts down from 9
B reverses at 3 toward 8
Bob's car call at 5 is now behind a DOWN car
B opens on 8; A also opens on 8
(8, DOWN) cannot be cleared twice without a uniqueness set

That is the parking-lot double book, in a shaft. The fix is the same shape: one claim.

synchronized (controllerLock):
  if (8, DOWN) already in pendingHall:
    return existing assignment          // Bob's second press
  pendingHall.add((8, DOWN))
  owner = cheapestCompatibleCar((8, DOWN))
  if owner: assignment[(8, DOWN)] = owner
  return owner

Unassigned is legal. Every step(), the controller retries assignment for hall calls with no owner (the cheap car was full, or in MAINTENANCE). It does not steal a call from a car that is already on the way unless that car faults.

idle nearest              simple; ignores a down car already one floor above
compatible + extra travel my default
look-ahead after reverse  fewer idle waits; easy to yo-yo on paper
random / round-robin      fair, dumb; use only as a tie-break

I would start with compatible + extra travel. I would not open a queueing-theory paper “because two cars.”

11. Request uniqueness

(floor, direction) is a set key, not a queue of passengers.

Alice presses 8 DOWN     pendingHall = { (8, DOWN) }
Bob   presses 8 DOWN     pendingHall = { (8, DOWN) }   // same lamp

When Car A opens going down on 8, the hall call is gone. Alice and Bob both board if load allows. If the car has room for one, Alice boards, Bob stays, and you must re-create or keep (8, DOWN) because the landing is still waiting. That is the Senior wrinkle: clearing the call is “we opened for this direction and were not full,” not “we touched floor 8.”

onStop(car, floor):
  openedForHall = matching hall call assigned to car
  if openedForHall and car.load >= car.capacity:
    do not remove the hall call
    reassign later
  else:
    pendingHall.remove(openedForHall)
    assignment.remove(openedForHall)
  car.carCalls.remove(floor)

Car calls are a set per elevator. Two passengers in B who both want 5 press one button. Uniqueness is (elevator_id, floor).

Idempotency:

requestHall(8, DOWN) twice     one pending row
requestCar(B, 5) twice         one car call
step() after the call is gone  no second open

requestHall is idempotent. step() is not “replay the press.”

12. Tick / step simulation

Interviews want a clock, not new Thread() per floor, per door, and per lamp. Real buildings have a PLC loop. Your whiteboard has step().

One tick does one physical action per car:

Controller.step():
  synchronized (lock):
    assignUnownedHallCalls()
    for car in elevators:
      car.tick()
Elevator.tick():
  if MAINTENANCE: return

  if state == DOOR_OPEN:
    if obstruction: reset dwell; return
    dwell -= 1
    if dwell > 0: return
    close door
    pickNext()
    return

  if state == MOVING_UP:
    floor += 1
    if shouldStop(floor): openAt(floor)
    else if stopsAhead() empty: pickNext()
    return

  if state == MOVING_DOWN:
    floor -= 1
    if shouldStop(floor): openAt(floor)
    else if stopsAhead() empty: pickNext()
    return

  if state == IDLE:
    pickNext()

shouldStop is true when this floor is a car call, or an assigned hall call whose direction matches the car.

pickNext:

if stopsAhead() not empty:
  keep direction, state = MOVING_*
else if opposite sweep not empty:
  reverse direction, state = MOVING_*
else if controller handed us a new hall call:
  face that floor, leave IDLE
else:
  state = IDLE

Between 4 and 5 there is no “fractional floor” unless they ask. Power cut mid-tick: treat the car as at floor or floor+1 (pick one), open if you can, enter MAINTENANCE, return its hall assignments.

Why not threads? Two cars advancing on two threads still need the same mutex on pendingHall. You have invented races and gained nothing the interviewer can grade. step() is deterministic: requestHall(8, DOWN), then one tick and A is on 8 with doors open. Car B’s ticks interleave in the same for car loop.

13. Concurrency

Buttons are events. step() is a timer. They share memory.

controllerLock guards:
  pendingHall
  assignment
  each elevator.carCalls, state, floor, load

One mutex on those sets is enough for a bank of eight. Per-car locks plus a hall-set lock is how you deadlock: A holds its lock and waits for hall; the controller holds hall and waits for A.

requestHall / requestCar     short critical section, then return
step()                       one critical section for assign + all ticks

Do not hold the lock while you “sleep 2 seconds for the door.” Door time is dwell ticks inside step().

The race that matters is the parking-lot race:

        Alice                      Controller                    Bob
          |                            |                           |
          |---- requestHall(8, DOWN) ->|                           |
          |                            |<-- requestHall(8, DOWN) --|
          |                            |                           |
          |              pendingHall does not yet have (8, DOWN)   |
          |                            |                           |
          |     add + assign A         |                           |
          |                            |   add? already present    |
          |                            |   return deduped, still A |
          |<-- 202  assigned A --------|                           |
          |                            |-------- 200  same A ----->|

If you “check then add” without the lock, both threads insert and both run cheapestCompatibleCar. You can survive that if the set stays a set — but both might also start a second car. The lock makes the claim one operation.

I would not introduce a concurrent queue library to look senior. I would name the mutex and the invariant: a hall call has zero or one owner.

14. Class sketch

Ideas, not a framework dump. Panels share one Controller.

Direction      UP | DOWN
HallCall       floor, direction
CarCall        elevatorId, floor

Door
  phase, dwell, obstruction
  onTick() / reopen()

Elevator
  id, floor, state, direction, load, capacity
  carCalls: Set<int>
  door: Door
  tick()
  shouldStop(floor) -> bool
  pickNext()
  acceptCarCall(floor)
  assignHall(HallCall)
  releaseAssignments() -> HallCall[]     // MAINTENANCE

Controller
  elevators: Elevator[]
  pendingHall: Set<HallCall>
  assignment: Map<HallCall, Elevator>
  requestHall(floor, dir)
  requestCar(elevatorId, floor)
  status()
  step()
  assignUnownedHallCalls()
  cost(elevator, HallCall) -> int

requestHall is the claim in section 10: validate, lock, dedup, add, cheapestCompatible, assign. requestCar never goes through the cost function. The passenger is already inside; add the floor to that car’s set. If the new floor is behind the car, it waits for the reverse — you do not spin the cabin because Bob forgot and pressed 2 while going to 9. Empty-car immediate reverse is a LOOK special case; say it.

Handlers do not set state = MOVING_UP. pickNext() does, from the sets.

15. Tests that matter

Skip getters. Test the invariant.

  1. Alice 8 DOWN, A idle on 9. requestHall(8, DOWN). After enough step() calls, A is on 8 with doors open, going down. B (on 2, MOVING_UP, car call 5) is not on 8 and has not reversed.
  2. Do not reverse a busy up car. Same fixture. Assert B’s floor is non-decreasing until 5. Bob’s car call is served.
  3. Dedup. Two requestHall(8, DOWN). pendingHall size 1. One owner.
  4. Opposite lamps. (8, DOWN) and (8, UP) are two rows. A down stop clears only Down.
  5. Full car. A is assigned, load == capacity. A may pass 8 or open and refuse boarding; (8, DOWN) stays pending. A second car or a later sweep takes it.
  6. Car call here. B idle on 2, requestCar(B, 2)DOOR_OPEN, no tour of the building.
  7. LOOK sweep. A going down owns 8 and 3. Charlie presses 9 UP. A does not go to 9 until 3 is done.
  8. Obstruction / maintenance. Blocked door: stay DOOR_OPEN, floor unchanged. B in MAINTENANCE returns hall assignments; they reassign to A.

Test (1) and (2) are the offer. If they only have time for one fixture, use the opening story.

16. Extensions

Stay on objects. Do not add a message bus.

Express / local. A car has serves: Set<int> or a min/max. cost is ∞ when f is not served. The controller is the same. A sky lobby is a floor that is in two banks — two controllers, not one god object.

Destination dispatch. The landing keypad says “I want 27,” not “Down.” The request is DestinationCall(from, to). The controller assigns a car and may skip lighting a shared Down lamp. Inside, car-call buttons can disappear. The state machine does not change; the compatible-direction rule becomes “this car’s planned stop list already includes from on the way to to.” Say that only if they ask. Default remains hall + car.

Weight, fire, zone parking. Kilograms use the same skip-hall rule. Fire: ignore hall calls, go to a designated floor, MAINTENANCE — mention skip. Idle cars that pre-position to the lobby are pickNext on empty sets, not a new product.

17. Final architecture / object diagram

One bank. Many panels. One controller. Many cars.

     Floor 8 Down             Floor 2 Up              Car B panel
          |                        |                       |
          |  requestHall           |                       | requestCar
          v                        v                       v
        +------------------------------------------------------+
        |                    Controller                        |
        |  requestHall / requestCar / status / step            |
        |  pendingHall  Set<(floor, dir)>                      |
        |  assignment   (floor, dir) → one Elevator            |
        +-------------------+----------------------------------+
                            |
            +---------------+----------------+
            |                                |
            v                                v
      Elevator A                       Elevator B
      floor, state                     floor, state
      carCalls set                     carCalls set
      Door                             Door
      tick / pickNext                  tick / pickNext
        Alice                    Controller           A            B
          |                          |                |            |
          |-- requestHall(8, DOWN) ->|                |            |
          |                          | assign A       |            |
          |                          |-- assignHall ->|            |
          |                          |                |            |
          |                          |<---- step() ---+---- step() |
          |                          |                | 2→3→4→5    |
          |                          |                | 9→8 open   |
          |                          | clear (8,DOWN) |            |
          |                          |                |            |
          |                     requestCar(A, 3)      |            |
          |                          |                | 8→3        |

Kafka, Redis, and “elevator microservice” are not in this picture. If those words appear, you left LLD. Compare Design a Parking Lot: one object owns the claim. Source of truth is pendingHall + carCalls + state under one lock. Arrows and status() are derived.

18. Interview-ready summary

How to walk through in 10–15 minutes

0–2 min. Alice on 8 Down, A idle on 9, B up from 2 to 5. LLD, not a cloud.
2–4 min. Floors, two cars, hall vs car buttons, capacity, skip emergency.
4–6 min. Entities: HallCall(floor, dir), CarCall, Elevator, Controller, Door.
6–9 min. Naive closest-each-tick yo-yo; LOOK commits a direction; FCFS contrast.
9–12 min. Cost function, A gets Alice, B does not reverse; uniqueness set.
12–15 min. step() clock, one mutex, the two tests, destination dispatch as an extension.

Key decisions to remember

  1. This is objects and a scheduler, not a distributed product.
  2. Many cars = many Elevators + one Controller.
  3. A hall call is (floor, direction), a set key, not a passenger list.
  4. A car call belongs to one cabin and is not re-assigned.
  5. LOOK / SCAN: finish the sweep, then reverse. Do not re-pick closest every tick.
  6. Assign the compatible car with least extra travel; idle nearest if nobody is compatible.
  7. Do not give (8, DOWN) to a car committed UP past 8; look-ahead is opt-in and spoken.
  8. Full cars skip hall stops; the hall call stays pending.
  9. Simulate with step(), not a thread per floor.
  10. One mutex on the pending sets. A hall call has zero or one owner.

Likely interviewer follow-up questions

  • What if Alice and Bob both press Down on 8?
  • Why not always send the nearest car?
  • How do you stop Car B from turning around for Alice?
  • Car is full when it arrives — is the hall call cleared?
  • Up and Down both lit on floor 8. One stop or two?
  • How would you unit-test this without sleeping?
  • Destination dispatch — what changes?
  • How do express cars fit?
  • Door blocked, then power cut — which state wins?
  • What if they ask for a whole skyscraper?

Senior-level points that differentiate the answer

  • Name the yo-yo before drawing classes. Classes without a committed direction are a vocabulary list.
  • Separate hall calls from car calls; do not store “people on floor 8.”
  • Treat LOOK as “stops ahead in this direction,” not “seek to the top floor.”
  • Call out look-ahead instead of hiding it inside a magic cost.
  • Full-car arrival must not delete the landing request.
  • step() as the clock: deterministic tests, no floor threads.
  • Same claim habit as the parking-lot stall: one lock, one owner.
  • Scale is more banks, not Kafka.

A 1–2 minute verbal answer

An elevator bank is hall calls, car calls, and cars. A hall call is the pair (floor, direction) — two people pressing Down on 8 share one pending request. Each car is a state machine: idle, moving up, moving down, door open, maintenance. I schedule with LOOK: keep going in the current direction, collect every stop I already own, then reverse. I do not re-pick the closest floor every tick; that yo-yos and strands people. A controller assigns each hall call to at most one car — the idle nearest, or the busy car that can take it with the least extra travel without reversing. I would not give Alice’s Down on 8 to a car committed upward with a passenger for 5. Simulation is a shared step() that moves each car one floor or one door action. Buttons and the clock share one mutex on the pending sets. I would test Alice on 8 Down with an idle car on 9, and assert the busy up car never reverses. A tower is several banks of this same design.

For how this sits next to other LLD and HLD prompts, see Design a Parking Lot, the questions hub, and the System Design Interview Complete Guide.