Design an Elevator System

The state-machine classic: elevator states, the SCAN scheduling insight, Strategy for dispatch, and the multi-elevator follow-ups.

LLDOODstate-patternscheduling

Scope it first

"Design an elevator" is two problems wearing one prompt: object model (states, requests, doors) and scheduling (which elevator answers, in what order). Clarify both: "N elevators, M floors, hall calls (up/down button on a floor) + cabin calls (floor button inside), optimize average wait — ignore weight sensors and maintenance mode, OK?"

The two request types are the first real modeling decision — they carry different information: a hall call is (floor, direction); a cabin call is just (floor). Conflating them breaks scheduling later.

Entities & relationships

  • ElevatorSystem is the facade: receives calls, delegates "who goes?" to a DispatchStrategy, ticks the world via step().
  • Elevator owns its position, direction, door and stop set — and is a textbook state machine: IDLE → MOVING_UP / MOVING_DOWN → STOPPED(doors) → …. Transitions live in one place; "can't move with doors open" becomes an invariant, not a scattered if.
  • DispatchStrategy (Strategy pattern, like the parking lot's spot assignment): nearest-available, least-loaded, same-direction-first — swappable without touching the elevator.

Here's that state machine drawn out — note what's missing: there is no arrow from MOVING to DOORS_OPEN's neighbor states that skips STOPPED, and no arrow out of DOORS_OPEN while moving. Illegal transitions simply don't exist in the diagram, which is the whole point of the State pattern:

Now drive that machine yourself. Step through a call → arrive → doors → idle cycle, then add an illegal event (an arrive while still Idle) and watch it get refused — the invariant "can't arrive when you never moved" is enforced by the absence of a transition, not a scattered if.

Elevator controllertime O(1) per eventspace O(states)
callUpcallDownarrivearrivecloseIdleUpDownDoors
events:callUparriveclosecallDownarriveclose

1/7Start in Idle. Each event is handled by the current state — the State pattern moves this branching out of one giant switch and into the state objects themselves.

state = Idle

The scheduling insight: SCAN, not FIFO

Serving requests in arrival order (FIFO) makes the cabin ping-pong: 2 → 9 → 3 → 8 wastes everyone's time. Real elevators run SCAN (the "elevator algorithm" — disk drives borrowed it from elevators): keep moving in the current direction, servicing every stop in that direction; reverse only when none remain.

The data structure follows from the algorithm: not a queue but two sorted sets per elevator — stops above (served ascending while going up) and stops below (descending while going down). In code:

Python
class Elevator:
    def __init__(self, id):
        self.id, self.floor, self.direction = id, 0, Direction.IDLE
        self.up_stops = SortedSet()      # or a min-heap
        self.down_stops = SortedSet()    # or a max-heap

    def add_stop(self, floor):
        if floor > self.floor: self.up_stops.add(floor)
        elif floor < self.floor: self.down_stops.add(floor)

    def step(self):                      # one tick of the state machine
        if self.direction == Direction.UP:
            self.floor += 1
            if self.floor in self.up_stops:
                self.up_stops.remove(self.floor)
                self.open_doors()        # state: STOPPED
            if not self.up_stops:        # exhausted this direction
                self.direction = (Direction.DOWN if self.down_stops
                                  else Direction.IDLE)
        # ... mirrored for DOWN

A simple, defensible DispatchStrategy: prefer elevators already moving toward the call in the same direction (cost = distance), then idle ones (distance), then opposite-direction ones (distance to their reversal point

  • back). Naming that cost function out loud is the senior move; the exact formula matters less.

Where the patterns live

  • State — the elevator's lifecycle. Guards illegal transitions (MOVING → doors open) structurally.
  • Strategy — dispatch policy, and door-dwell policy if pressed.
  • Observer — floor displays and hall-button lamps subscribe to elevator events rather than being polled.
  • Singleton (resist it) — one ElevatorSystem, but inject it; tests will thank you.
Drive the design with `step()`

Making time explicit — a step()/tick method — turns the whole system into something you can unit-test deterministically ("after 3 ticks, elevator 2 is at floor 5 with doors open") and demo in the interview without threads. Real-time is then just a loop calling step() on a timer.

Think it through like the interview

Think it through: Design an Elevator SystemLLD Classic0/5 stages

PROBLEMDesign the classes and scheduling for N elevators serving M floors. Hall calls (up/down on a floor) and cabin calls (floor button inside) arrive continuously; minimize average wait.

  1. 1

    Split the problem

    This prompt hides two different problems. Can I name them before designing?

  2. 2

    Find the state machine

    An elevator can't move with doors open. Where should that rule LIVE?

    unlocks after the stage above
  3. 3

    Model the two request types

    Hall call vs cabin call — same class or different? What data does each carry?

    unlocks after the stage above
  4. 4

    Derive the algorithm, then the data structure

    Requests at floors 2, 9, 3, 8 from floor 1 — what order do I serve them, and what structure holds them?

    unlocks after the stage above
  5. 5

    Walk a scenario + break it

    Floor 6 presses DOWN; A is at 3 going up with stops {7,9}; B idle at 8. Who goes — and what's the race?

    unlocks after the stage above

Walk a scenario + the concurrency question

"User on floor 6 presses DOWN; elevator A is at 3 going up with stops 9; B is idle at 8." Dispatch: A is moving away-then-toward (cost ≈ (9−3) + (9−6) = 9), B is idle at distance 2 → B. B: IDLE → MOVING_DOWN, stops 6; arrives, doors open; user presses 1 → cabin call adds 1 to down-stops; continues down. SCAN means if someone on 4 presses DOWN meanwhile, B collects them en route — no extra trip.

Concurrency follow-up: hall calls arrive from many floors while elevators tick. Same answer family as the parking lot's last spot: serialize mutations — a lock (or single-threaded event loop) around assign-and-add-stop so a call is assigned exactly once, and make step() atomic per elevator. One elevator's state is touched by one thread at a time.

Practice — level up

An elevator controller is a scheduler: given the pending requests and the car's position, pick the next stop — then serve a sweep efficiently. These drills are that same "choose the next job" decision.

Practice ladder: Scheduling & dispatch0/4 solved

Climb in order — every rung assumes the one above it. Solve on LeetCode, then tick it here; progress is saved on this device.

Warm-up — pick the next job

  1. When free, take the best pending task by a policy — exactly 'which floor next'.

Core — assign over time

Many requests, limited movers, a clock.
  1. Route each request to a free resource as resources free up — multi-car dispatch.

  2. Order work under a cooldown constraint — sequencing a sweep of stops, not just choosing one.

Stretch — concurrent demand on a shared resource

  1. Peak simultaneous demand over a timeline — how many cars a request pattern actually needs.