Skip to content
FollowtheDamnCode

LLD: Elevator System

Several cars, many floors, and a dispatcher deciding who goes where — state per car, a scheduling strategy, and a step-by-step simulation.

Big idea

An elevator system is a dispatcher plus several cars, each a small state machine. Calls from floors go to the dispatcher, which picks a car by a scheduling strategy; each car keeps a set of stops and moves one floor per tick, continuing in its direction while it has stops ahead (the classic "elevator algorithm"). Separate who serves a call from how a car moves and both become easy to change.

Requirements

Functional

  • N cars, F floors.
  • Hall calls: someone on a floor presses up or down.
  • Car calls: someone inside presses a destination.
  • Cars move one floor per tick, stop at requested floors, open doors.
  • A dispatcher assigns each hall call to one car.

Non-functional: minimise waiting time; never skip a requested stop; safe under concurrent calls.

Out of scope: weight limits, emergency modes, maintenance (discussed as extensions).

Entities

ElevatorSystem (facade/clock), Dispatcher (assigns hall calls; a Mediator between floors and cars), DispatchStrategy (nearest car, least busy…), Car (floor, direction, stops), Direction (UP, DOWN, IDLE), Request.

Think of it like

A taxi dispatcher with a radio. Customers call the office, not the drivers. The dispatcher looks at where every cab is and which way it's heading and sends the best one. Each driver just keeps a list of pickups and drop-offs and works through them in a sensible order, not zig-zagging across town.

Class diagram

Elevator classes

Key flow: a hall call

A call from floor 7

Code skeleton

Two cars serving calls tick by tick
class Car:
    def __init__(self, cid, floor=0):
        self.id, self.floor, self.direction, self.stops = cid, floor, "IDLE", set()

    def add_stop(self, floor):
        self.stops.add(floor)

    def cost(self, floor):               # used by the strategy: distance, plus a penalty for moving away
        away = (self.direction == "UP" and floor < self.floor) or (self.direction == "DOWN" and floor > self.floor)
        return abs(self.floor - floor) + (10 if away else 0) + len(self.stops)

    def step(self):
        if not self.stops:
            self.direction = "IDLE"
            return None
        if self.floor in self.stops:     # arrived
            self.stops.discard(self.floor)
            return f"car {self.id} opens at {self.floor}"
        if self.direction == "IDLE":     # start towards the nearest stop
            nearest = min(self.stops, key=lambda s: (abs(s - self.floor), s))
            self.direction = "UP" if nearest > self.floor else "DOWN"
        ahead = [s for s in self.stops if (s > self.floor if self.direction == "UP" else s < self.floor)]
        if not ahead:                    # nothing left this way: reverse (the elevator algorithm)
            self.direction = "DOWN" if self.direction == "UP" else "UP"
        self.floor += 1 if self.direction == "UP" else -1
        return None


class NearestCar:                        # a DispatchStrategy
    def choose(self, cars, floor):
        return min(cars, key=lambda c: (c.cost(floor), c.id))


class ElevatorSystem:
    def __init__(self, n_cars, strategy):
        self.cars = [Car(i) for i in range(n_cars)]
        self.strategy, self.time = strategy, 0

    def hall_call(self, floor):
        car = self.strategy.choose(self.cars, floor)
        car.add_stop(floor)
        print(f"t={self.time}: call at {floor} -> car {car.id}")

    def car_call(self, cid, floor):
        self.cars[cid].add_stop(floor)

    def tick(self):
        self.time += 1
        for car in self.cars:
            event = car.step()
            if event:
                print(f"t={self.time}: {event}")


system = ElevatorSystem(2, NearestCar())
system.cars[1].floor = 8
system.hall_call(6)
system.hall_call(1)
for _ in range(3):
    system.tick()
system.car_call(1, 0)
system.car_call(0, 4)
for _ in range(9):
    system.tick()
print("positions:", [(c.id, c.floor, c.direction) for c in system.cars])
output
t=0: call at 6 -> car 1
t=0: call at 1 -> car 0
t=2: car 0 opens at 1
t=3: car 1 opens at 6
t=7: car 0 opens at 4
t=10: car 1 opens at 0
positions: [(0, 4, 'IDLE'), (1, 0, 'IDLE')]

Run and edit it in the app.

Each car's movement rule and the dispatcher's choice are independent: try a "least busy" strategy and the cars' code doesn't change.

Watch it

A car at floor 5 going up has stops at 8 and 2. Under the elevator algorithm, what order does it serve them?

Morning rush: most calls are from the lobby going up. Where do you change behaviour to park idle cars at the lobby?

Extensions and trade-offs

  • Car state: doors, moving, idle, maintenance → a proper State machine per car.
  • Smarter dispatch: estimated-time-of-arrival including stops; destination dispatch (passengers enter destination in the lobby).
  • Capacity: skip hall calls when full; the strategy must know load.
  • Concurrency: calls arrive from many threads; a single dispatcher loop with a queue avoids races.
  • Observability: Observer events for floor displays and logs.

Key terms

  • Hall call — A request from a floor button, with a direction.
  • Car call — A destination pressed inside a car.
  • Elevator algorithm (SCAN) — Keep moving in one direction while there are stops ahead, then reverse.
  • Dispatcher — The component that assigns hall calls to cars.