Skip to main content

Command Palette

Search for a command to run...

Design an Elevator System | LLD System Design

Updated
17 min readView as Markdown

"Design an elevator system" is one of the most common low-level design prompts, and it is a good one because it has two hard parts hiding behind a boring object: how does a single car decide where to go next, and which car answers a call when there are several. Everything else is plumbing. This post takes it end to end the way an interview would - clarify, list requirements, find the classes, draw the design, and write real code only for the part that is actually hard.

Start by clarifying, not coding

The prompt is deliberately vague, and the common mistake is to picture one shaft in a small office and start coding. An elevator system is a spectrum: a three-floor walk-up at one end, and a tower like the Burj Khalifa at the other - dozens of cars split into separate banks, sky lobbies, express zones, and destination-dispatch keypads in the lobby. You will not implement the top of that range in an hour, but your questions have to show you know it is there. Map the space first, then scope down on purpose. Here is the set of questions worth asking, each with the kind of answer to expect and what it settles.

Scale and layout. "How many floors and cars? One elevator bank, or several - and are there sky lobbies or express zones where a car only serves a band of floors?" Interviewer: One bank of N cars serving all F floors, contiguous, no zoning. That answer collapses a lot: every car can serve every request, so dispatch is a pure assignment problem rather than routing-plus-zoning.

The call model - this is the big one. "Traditional controls, where each floor has up/down buttons and you pick your floor after boarding? Or destination dispatch, where you enter your destination on a lobby keypad and the system tells you which car to take and groups passengers heading to nearby floors?" Interviewer: Traditional up/down hall calls plus in-car floor buttons. Ask this because destination dispatch - what most modern high-rises, the Burj Khalifa included, actually run - moves the assignment to before boarding and reshapes the whole controller. Naming it and deferring it is a strong signal by itself.

What are we optimizing? "Average passenger wait time, worst-case wait, throughput during a rush, or energy? And do we care about traffic patterns - up-peak in the morning, down-peak in the evening - where idle cars should pre-position?" Interviewer: Minimize average wait, and keep the policy swappable. That is the interviewer handing you the main design decision: the dispatch policy is a variation point, so it belongs behind an interface, not baked into the controller. Traffic-aware parking of idle cars then becomes just another strategy you can slot in later.

Capacity. "Do cars have a weight or headcount limit, and should a full car stop accepting hall calls?" Interviewer: Assume a limit exists, but do not optimize around it. Leave a seam - a capacity check on the boarding path - and move on.

Special and priority modes. "Maintenance lockout, fire-service recall to the ground floor, freight or VIP priority?" Interviewer: They exist, but they are not the focus. Name the cost and defer: a car carries a status (IN_SERVICE, MAINTENANCE, and so on) that dispatch skips over, and the rest is out of scope. Do not spend your 45 minutes modeling a fire alarm.

Concurrency. "Real-time and multi-threaded, or can I model time as discrete ticks?" Interviewer: Ticks are fine. That one is a gift - a step() that advances the world by one unit of time turns a scary concurrency problem into a simulation you can actually finish and run.

So the version I will actually build: one bank of N cars, F contiguous floors, traditional hall and car calls, one solid per-car movement algorithm, and pluggable group dispatch - with capacity, special modes, zoning, and destination dispatch named as seams rather than code. That is the same skeleton the tower has; the Burj Khalifa just has far more of it.

Requirements, written down

Functional:

  • A building has F floors and N elevator cars.
  • A passenger on a floor makes a hall call (a floor plus a direction, up or down); the system assigns exactly one car to answer it.
  • A passenger inside a car makes a car call (a destination floor).
  • A car serves every stop in its current direction before it reverses - it does not flip direction while requests remain ahead of it. This is the LOOK algorithm (the same idea as disk-head scheduling: sweep one way serving stops, then turn around).
  • Doors open at each stop.

Non-functional:

  • The dispatch policy (which car answers) must be swappable.
  • Keep room for capacity, modes, and richer policies later, without rewriting the core.

Explicitly out of scope (named as seams, not built):

  • Destination-dispatch controls (assignment before boarding).
  • Multiple banks, sky lobbies, and express zones.
  • Capacity/weight enforcement and full-car behavior.
  • Maintenance, fire-recall, and priority modes.

Turn the nouns into classes, and do not throw the small ones away

This is where a lot of candidates lose the round. They "simplify" the model down to four or five classes - Elevator, ElevatorSystem, a strategy, an enum - and wave off a button as "just a method." An LLD interviewer reads that as under-modeling, and it is a real rejection reason: the round is testing whether you can decompose a physical system into objects with single responsibilities. So model the hardware. A real elevator is full of parts, and most of them earn a class.

Underline every noun and keep the physical ones:

  • Building - owns the floors and the cars.
  • Floor - a floor number, its outer call panel, and a display.
  • Elevator (the car) - id, current floor, state, and the parts bolted to it: a door, a display, and its inner panel.
  • HallPanel (the outer panel, one per floor) - the up and down buttons a waiting passenger presses.
  • ElevatorPanel (the inner panel, one per car) - a button per floor, plus door-open and door-close buttons.
  • Button - it has real state (pressed or not) and behavior (press(), reset()), so it is an abstract base with concrete subtypes: HallButton (carries a direction), ElevatorButton (carries a destination floor), and DoorButton (open or close). Spotting the shared Button abstraction and its three specializations is exactly the modeling the interviewer is looking for.
  • Door - opens and closes, and holds a DoorState.
  • Display (the LCD panel) - shows the current floor and direction; the same class hangs on a floor and inside a car.
  • Direction, DoorState, ElevatorState - enums.

None of these is "too small." A Button that is only a boolean and two methods still belongs on the diagram, because leaving it off is the difference between "modeled the system" and "modeled a spreadsheet." You will not write code for every one of them in the hour - most are thin holders - but you draw all of them.

That is the structural half. The behavioral half is the classes that decide things:

  • ElevatorSystem - the entry point: takes a hall call or a car call from the outside and drives the simulation tick.
  • Dispatcher - the single job of choosing which car answers a hall call. It is its own class because it is its own responsibility (SRP), and because that decision varies.
  • SchedulingStrategy - and because it varies, it hides behind an interface: nearest car today, a load-balanced or traffic-aware policy tomorrow. Dispatcher depends on the interface, never on a concrete algorithm - the Dependency Inversion Principle and a textbook Strategy pattern. Swap the strategy, not the dispatcher.
  • Request - a hall call as data: a floor plus a direction, handed to the dispatcher.

The design, in two views

The model is big enough that one box-and-line drawing would be an unreadable hairball, so split it the way the system actually splits: the structure (the physical hardware and how it composes) and the control (who decides where cars go). Drawing two clean diagrams instead of one crowded one is itself a signal that you see the seam between them.

Structure - the building and its hardware:

Structural UML class diagram: Building composes Floors and Elevators; each Floor has a HallPanel and a Display; each Elevator has a Door, a Display and an ElevatorPanel; panels compose Buttons; Button is an abstract base for HallButton, ElevatorButton and DoorButton

Read the relationships off it, because they are half the interview:

  • Composition (filled diamond): a Building owns its Floors and Elevators; an Elevator owns its Door, Display, and ElevatorPanel; a panel owns its Buttons. These parts have no life apart from the whole - scrap the car and its door goes with it.
  • Inheritance (hollow triangle): HallButton, ElevatorButton, and DoorButton all extend the abstract Button, reusing its pressed-state and press()/reset() behavior.
  • Multiplicity: a HallPanel has exactly 2 buttons (up, down); an ElevatorPanel has F floor buttons; a Building has N cars.

Control - dispatch and movement:

Control UML class diagram: ElevatorSystem composes a Dispatcher and references N Elevators; Dispatcher holds a SchedulingStrategy realized by NearestElevatorStrategy and depends on Request; Elevator depends on the ElevatorState enum

  • ElevatorSystem composes the Dispatcher and references the Elevators (hollow diamond - the Building owns the cars, the system just drives them).
  • Dispatcher holds a SchedulingStrategy (aggregation): injected, swappable, living independently of the dispatcher.
  • NearestElevatorStrategy realizes SchedulingStrategy (dashed triangle): drop in another policy and nothing else changes.

Elevator is the one class that appears in both views. It is the bridge between the hardware and the control logic, which is why the interesting code lives inside it.

From a full diagram to the code you actually write

You draw all eighteen-odd classes, but you do not implement all of them in an hour, and no interviewer expects you to. The physical classes - Button, Door, Display, HallPanel, Floor - are thin holders: a field or two and a setter. They cost nothing to list and nothing to skip in code. The two boxes that carry real logic are the car (Elevator.step()) and the dispatch decision, so those are the ones worth writing out. Here is the behavioral core in pseudocode - what you would put on the whiteboard first. (One honesty note: the diagram gives dispatch its own Dispatcher class for SRP; the code below folds it into ElevatorSystem to keep the listing short. Split it out when you have the time.)

class Elevator:
    id
    currentFloor = 0
    direction    = IDLE
    up   = empty set   # stops to make while heading up
    down = empty set   # stops to make while heading down

    addStop(target):
        if target above currentFloor: up.add(target)
        else:                         down.add(target)
        if direction == IDLE: face toward target

    step():            # <-- the conflicting logic, shown for real below
        move one floor in current direction
        if this floor is a stop: open doors, remove it
        if nothing left ahead: turn() or go IDLE

interface SchedulingStrategy:
    pick(elevators, floor) -> Elevator

class ElevatorSystem:
    elevators   # the N cars, created up front
    strategy    # a SchedulingStrategy

    requestElevator(floor):              # hall call
        car = strategy.pick(elevators, floor)
        car.addStop(floor)
        return car.id

    selectFloor(carId, floor):           # car call
        elevators[carId].addStop(floor)

    step():
        for each car: car.step()

Notice how little the controller does. It routes calls and ticks the clock. All the difficulty is inside two methods, and those are the only two worth writing out in full.

Conflict one: where a single car goes next

This is the part people get wrong. The naive version answers requests first-come-first-served, which sends the car bouncing across the building. LOOK fixes that by keeping stops in two sets - the floors above (serve while going up) and the floors below (serve while going down) - finishing one direction entirely before reversing.

step(): void {
  if (this.direction === Direction.Up) {
    this.floor++;
    if (this.up.delete(this.floor)) this.openDoor();
    if (this.up.size === 0) this.turn();          // nothing higher to serve
  } else if (this.direction === Direction.Down) {
    this.floor--;
    if (this.down.delete(this.floor)) this.openDoor();
    if (this.down.size === 0) this.turn();         // nothing lower to serve
  }
}

private turn(): void {
  if (this.direction === Direction.Up && this.down.size > 0) this.direction = Direction.Down;
  else if (this.direction === Direction.Down && this.up.size > 0) this.direction = Direction.Up;
  else this.direction = Direction.Idle;            // no work left anywhere
}

The whole algorithm is those two methods. A car going up keeps climbing, deleting stops as it passes them, until the up set is empty; only then does turn() decide whether there is downward work to do or whether the car should go idle. It never reverses with stops still ahead of it, so there is no wasted travel.

The refinement to mention out loud: this lean version buckets a stop purely by whether it is above or below the car right now, which drops the hall call's own direction. A passenger on floor 8 pressing down while the car is climbing to floor 9 gets treated the same as one pressing up. The fully correct fix keys each stop by (floor, direction) and only picks up a hall call when the car's travel direction matches - plus a special case for the turning floor. Say that in the interview and note the cost (one more field on the request, a slightly longer step()); implement it only if there is time. Knowing the limit of your own simplification is worth more than the extra ten lines.

Conflict two: which car answers the call

The second decision lives entirely behind the interface. The naive version is one line - pick the car with the smallest abs(currentFloor - callFloor) - and it is wrong in a way interviewers love to catch: a moving car cannot stop on a dime. If a car is climbing past floor 5 and someone presses up on floor 6, that car is closest, but it is already inside its braking distance and will sail right past. Assigning it means the caller waits for a full round trip. So the real policy is the nearest car that can actually stop for the call on this pass.

class NearestElevatorStrategy implements SchedulingStrategy {
  pick(elevators: Elevator[], call: HallCall): Elevator {
    const ready = elevators.filter(
      (e) =>
        e.canServeOnThisPass(call.floor) &&
        (e.travelDirection === Direction.Idle || e.travelDirection === call.direction),
    );
    // If no car can catch it this pass, fall back to nearest; it is served on a later sweep.
    const pool = ready.length > 0 ? ready : elevators;
    return pool.reduce((best, e) =>
      Math.abs(e.currentFloor - call.floor) < Math.abs(best.currentFloor - call.floor) ? e : best,
    );
  }
}

The braking rule itself lives on the car, because only the car knows its own physics:

static readonly BRAKING_DISTANCE = 2; // floors of runway needed to stop

canServeOnThisPass(floor: number): boolean {
  if (this.direction === Direction.Idle) return true;
  if (this.direction === Direction.Up) return floor >= this.floor + Elevator.BRAKING_DISTANCE;
  return floor <= this.floor - Elevator.BRAKING_DISTANCE; // moving down
}

pick now filters out cars that are moving the wrong way or are too close to brake, takes the nearest of what is left, and only falls back to raw distance when nothing can catch the call this pass (that car grabs it on a later sweep). Because ElevatorSystem only ever sees SchedulingStrategy, a smarter policy later - load-balancing across zones during a morning rush, or parking idle cars at busy floors - is a new class, not an edit to the controller.

The edge cases that separate a pass from a fail

Once the happy path works, a good interviewer starts poking at it. The move is to name every one of these out loud, then solve only the one or two that carry real logic and say plainly that the rest are guards you would add with more time. Listing them is most of the credit; coding all of them is neither expected nor a good use of the hour.

Already covered by the design as written - worth pointing at, since they fall out of the data structures for free:

  • Too close to stop. A hall call for a floor a moving car is about to pass, inside its braking distance, cannot be served by that car this pass. canServeOnThisPass filters it out of dispatch, so a farther car that can stop wins. (The one we coded.)
  • A repeated press. Someone jabs the up button five times. A car holds its stops in a Set, so re-adding the floor is a no-op - the button is idempotent, no duplicate travel.
  • No car can take it right now. If every car is busy, the call does not vanish; the pool fallback in pick hands it to the nearest car to grab on a later sweep. Never return "no elevator."

Name and defer - real, but not worth code time in a 60-minute round:

  • A press for the floor a car is sitting on. Idle at that floor: open the doors now. Moving through it: that is just the braking case with zero runway.
  • A call in the opposite direction. A car climbing to floor 9 passes floor 8 where someone wants to go down; it should catch them on the way back, not stop and mislead them. The lean model buckets stops by position, not call direction - the simplification to flag (see the note under Conflict one) and fix with a (floor, direction) key only if time allows.
  • A full car. At capacity, stop accepting hall calls until someone exits - a load counter plus one more guard in canServeOnThisPass.
  • A floor out of range or a car in maintenance - a validation check at the entry point and a status flag dispatch skips.

That split is the point: show the braking case working, and let the rest be a confident list. Enumerating them is what the round is really checking.

Running it

Two cars. A hall-up call at floor 3, the passenger boards and picks floor 9, then a hall-up call lands at floor 6 while car 0 is climbing past floor 5:

Hall call at floor 3 (UP) -> elevator 0
tick 1: E0@1[UP]  E1@0[IDLE]
  Elevator 0: doors open at floor 3
...
tick 5: E0@5[UP]  E1@0[IDLE]
Hall call at floor 6 (UP) -> elevator 1  (car 0 too close to brake, skipped)
tick 6: E0@6[UP]  E1@1[UP]
  Elevator 0: doors open at floor 9
tick 9: E0@9[IDLE]  E1@4[UP]

Car 0 is only one floor below the floor-6 call and is the closer car, but it is inside its braking distance of 2, so dispatch skips it and the idle car 1 takes the call and climbs to answer. That skip is the whole point: the closest car is not always the right car. The full runnable TypeScript - the Elevator, the braking-aware strategy, the controller, and this demo - is in the system-design-guide repo under lld/elevator.

What the interviewer is actually grading

A machine-coding round is not asking for a working elevator. It is checking whether you can take a vague prompt, model it into real objects, put the varying behavior behind an interface, and write the hard 30 lines cleanly in the time you have. For this problem that means all three of: clarify the range from a walk-up to destination dispatch and scope down out loud; model the whole thing - building, floor, car, door, display, and the panels and buttons, with Button as an abstraction over its subtypes - and do not shrink it to five boxes, because that is the fastest way to fail the design portion; then get LOOK and the dispatch strategy right in code. Draw the eighteen classes, name "Strategy" and "Dependency Inversion" as you draw them, implement the two that carry logic, call out the edge cases - braking distance, repeated presses, a full car, no free car - and defer destination dispatch, zoning, and capacity explicitly. That is the whole round.

Next in this series we will take the same process to a vending machine, where the interesting part is not an algorithm but a state machine - and the State pattern earns its keep.

More from this blog

P

Programming, System Design and AI | Latencot

53 posts

Here, you will read all about tech and tech updates. From writing code to building AI systems, we'll talk and share about everything.