Elevator System

Advanced · 18 min read · ▶ live playground · ✦ checkpoint

“Design an elevator system for one building with N cars and M floors.” The car moving one floor is the easy line of code. The interview is testing two different decisions: which car receives a hall call, and how that car changes behavior while serving its own stops.

Requirements

Separate group dispatch from per-car movement before drawing either one. A fast local queue cannot rescue a poor car assignment, and a clever dispatcher cannot move a car whose state transitions are inconsistent.

KindRequirement for this design
FunctionalOne building has N elevator cars serving floors 0 through M - 1.
FunctionalA rider can place an external hall call with a floor and an UP or DOWN direction.
FunctionalA rider inside a car can request a destination floor.
FunctionalA dispatcher assigns exactly one car to each accepted hall call.
FunctionalEvery car keeps its own pending stops and serves them in directional-sweep order.
FunctionalA car moves one floor per simulation step, opens its doors at a requested floor, and then continues or reverses.
FunctionalThe system reports every car's current floor, direction, and behavioral state.
FunctionalBoundary hall calls are valid only when travel exists in that direction: no DOWN at floor 0 and no UP at the top floor.
Non-functionalDispatch policy must be replaceable without changing car movement.
Non-functionalA hall call must not be assigned twice when dispatch requests race.

Door obstruction sensors, weight limits, fire-service mode, maintenance controls, and real motor control are out of scope. The skeleton models scheduling decisions and discrete steps; it does not command physical equipment.

Clarifying questions to ask

Each answer chooses an algorithm boundary, a state transition, or an invariant.

  • How many cars and floors exist? Both are constructor inputs. Floors use zero-based integers from 0 through M - 1; car IDs are unique within the building.
  • Is the dispatch policy fixed? No. ElevatorSystem receives a DispatchStrategy. The baseline chooses the nearest suitable car, while a later policy could use zoning, estimated arrival time, or peak-hour rules.
  • What makes a moving car suitable for a hall call? An idle car is suitable. A moving car is suitable when its direction matches the call and the pickup floor is still ahead. Unsuitable cars remain fallbacks so a call is never dropped when every car currently moves the other way.
  • How does a car order its own stops? The baseline uses a LOOK-style directional sweep: continue serving stops ahead in the current direction, then reverse at the furthest pending stop. It does not process the stop list in insertion order.
  • Is that SCAN or LOOK? Both are directional-sweep families. SCAN continues to a boundary before reversing; LOOK reverses at the furthest outstanding request. The skeleton implements LOOK because an empty trip to the building boundary adds no value here.
  • How are UP and DOWN hall calls distinguished? Direction expresses the rider's intended journey and affects car suitability. The pickup floor becomes an assigned stop; after boarding, the rider's internal destination adds the concrete next stop.
  • Does a car skip an opposite-direction caller while passing the floor? A production scheduler may retain pickup direction and defer that stop until the matching sweep. This interview baseline uses direction during dispatch, then treats the assigned pickup as a floor stop. State that simplification rather than claiming full destination control.
  • When is a hall call served? When its assigned car enters DoorsOpenState at the pickup floor. Merely passing the floor or assigning a car does not complete the call.
  • Are duplicate hall calls coalesced? A production boundary should keep one pending call per (floor, direction) and return its current assignment. The compact skeleton gives every submitted call an ID so the dispatch and movement responsibilities stay visible.
  • What does an internal car request contain? Only a destination floor and the car identity. Its travel direction follows from destination versus current floor, so it does not carry a second Direction field.
  • Should idle cars return to the lobby? Not in the baseline. An idle car stays at its last floor. Lobby parking, sky-lobby parking, and distributed idle placement are dispatch or repositioning policies with energy and response-time trade-offs.
  • How long do doors remain open? One simulation step represents DoorsOpenState; the next step closes the doors and chooses the next motion state. Real dwell timers and obstruction events are excluded with the sensors.
  • Can calls arrive while a car is moving? Yes. Stops ahead join the current sweep; stops behind wait for the reverse sweep. Commands for one car must be serialized so addStop() and step() do not race over its queue.
  • What happens when calls arrive concurrently? Assignment needs a shared pending-call claim. A process-local lock inside one ElevatorCar cannot stop two dispatch workers from assigning the same hall call to different cars.

Entity extraction

Run the noun pass from requirements-to-classes before turning every button into a class:

  1. An elevator system in one building coordinates several elevator cars across floors.
  2. A rider creates a hall call with a direction or an internal car request with a destination.
  3. A dispatcher policy assigns one car to a hall call.
  4. Each car owns a request queue, a current position, and an elevator state.
  5. The car may be idle, moving up, moving down, or have doors open.

Sort those words by identity, value, actor, and varying behavior:

BucketNounsModel choice
Contextbuilding, elevator systemOne ElevatorSystem owns the building-wide car collection and call boundary
Domain entityelevator carElevatorCar has identity, position, queue, and a changing lifecycle
Request valueshall call, car request, floorHallCall and typed method inputs; a per-car StopQueue stores destination floors
EnumdirectionDirection contains UP, DOWN, and IDLE for reports and state behavior
Replaceable policydispatcher policyDispatchStrategy with NearestSuitableCarStrategy as the baseline
State contractelevator stateElevatorState with idle, moving-up, moving-down, and doors-open implementations
ActorriderPresses hall or car buttons but does not become a core domain class
Excluded conceptsmotor, sensor, load, fire modeHardware and emergency-control boundaries stay outside this model

The verb pass separates group responsibility from car responsibility:

Verb phraseOwnerReason
accept and validate a hall callElevatorSystemIt knows the building floor range and the call lifecycle boundary
choose a carDispatchStrategyAssignment algorithms vary independently from movement
add and order a stopElevatorCar through StopQueueThe car owns its pending work and current position
move one flooractive ElevatorState coordinates; ElevatorCar changes positionState decides what a step means, while the car protects its shared position
open and close doorsDoorsOpenState and the adjacent motion stateDoor behavior is part of the car's internal lifecycle
choose when to reversemoving state queries StopQueueThe current sweep direction and pending stops determine the transition
report position and directionElevatorCarThose facts belong to one car and must agree with its active state

DispatchStrategy uses the Strategy pattern from strategy: configuration chooses an interchangeable assignment algorithm. ElevatorState uses the State pattern from state: operations move the car through its own IdleState → MovingUpState → DoorsOpenState lifecycle. Their class shapes look similar, but their reasons for change are different.

Core class diagram

Keep both seams in one ten-node core. The per-car StopQueue remains a field-level collaborator in this diagram; its sorted-set implementation appears in the skeleton.

class diagram

Core elevator dispatch and car-state model

Core elevator dispatch and car-state model
  1. 1ElevatorSystem owns the building-wide call boundary and car collection.
  2. 2The system delegates each hall-call assignment through DispatchStrategy.
  3. 3NearestSuitableCar supplies the baseline direction-aware score.
  4. 4A HallCall preserves pickup floor and intended travel direction.
  5. 5The system contains Cars, while each Car owns its position and stop queue.
  6. 6A dispatch policy reads car reports but does not move the selected car.
  7. 7ElevatorCar delegates each simulation step to its current ElevatorState.
  8. 8IdleState waits or chooses a direction toward the nearest pending stop.
  9. 9MovingUpState serves higher stops before considering a reversal.
  10. 10MovingDownState mirrors the sweep for lower pending stops.
  11. 11DoorsOpenState marks arrival, then closes and selects the next motion state.

The system owns cars and a dispatch strategy through separate relationships. The policy may inspect a car's report, but only ElevatorSystem assigns the stop and only the car's current state changes its position.

IdleState, MovingUpState, MovingDownState, and DoorsOpenState carry no duplicate queues. Position and pending stops survive every transition on ElevatorCar; states decide what the next step() means and install their successor.

Critical-flow sequence diagram

The critical flow is “hall call → car assignment → arrival.” The repeated movement ticks are condensed around the final downward step so the assignment and state transition remain visible together.

sequence diagram

Hall call is assigned and served by one car

Hall call is assigned and served by one car
  1. 1The system validates the directional hall call, then asks one policy to assign it.
  2. 2The strategy compares reports and returns car B without mutating any car.
  3. 3Only after selection does the system add floor 7 and return the assignment.
  4. 4On the final tick, MovingDownState advances the car to its requested floor.
  5. 5Arrival installs DoorsOpenState and publishes the served floor to the rider.

The dispatch strategy is read-only during selection. That keeps scoring free of half-applied assignments: after one car wins, the system claims the pending hall call and appends the stop together. A durable design should make that claim atomic before returning “assigned car B.”

DoorsOpenState is the completion boundary. If the process crashes after assignment but before arrival, the call remains pending and can be recovered; marking it served at message 7 would lose the rider while the car was still several floors away.

SCAN and LOOK are interview baselines

SCAN serves requests in one direction and travels to that direction's boundary before reversing. LOOK also serves the current direction first, but reverses at the furthest pending request. Both avoid the reversal thrash of naive first-come, first-served movement, and LOOK avoids an empty trip to floor 0 or M - 1.

The skeleton uses LOOK over a sorted stop set. It is a clean interview baseline, not a claim about what Otis ships. Real group control is a tuned, often proprietary optimization problem involving estimated time to destination, door and passenger delays, zoning, demand prediction, energy use, and service guarantees. Even LOOK can delay the opposite direction under a continuous stream of requests; aging or a maximum-wait rule belongs in a stronger scheduler.

TypeScript skeleton

The skeleton implements a replaceable nearest-suitable dispatch policy, a per-car sorted stop set, four State objects, and one-floor simulation steps.

export {};
 
enum Direction {
  Up = "up",
  Down = "down",
  Idle = "idle",
}
 
interface HallCall {
  readonly id: string;
  readonly floor: number;
  readonly direction: Direction.Up | Direction.Down;
}
 
interface CarReport {
  readonly carId: string;
  readonly floor: number;
  readonly direction: Direction;
  readonly state: string;
  readonly pendingStops: readonly number[];
}
 
type CarStepKind =
  | "idle"
  | "moved"
  | "doors-open"
  | "doors-closed";
 
interface CarStep {
  readonly carId: string;
  readonly floor: number;
  readonly state: string;
  readonly kind: CarStepKind;
}
 
class StopQueue {
  private readonly stops = new Set<number>();
 
  public add(floor: number): void {
    this.stops.add(floor);
  }
 
  public take(floor: number): boolean {
    return this.stops.delete(floor);
  }
 
  public hasAbove(floor: number): boolean {
    return [...this.stops].some((stop) => stop > floor);
  }
 
  public hasBelow(floor: number): boolean {
    return [...this.stops].some((stop) => stop < floor);
  }
 
  public ascending(): readonly number[] {
    return [...this.stops].sort((a, b) => a - b);
  }
 
  public directionToNearest(floor: number): Direction {
    const ordered = this.ascending();
    const first = ordered[0];
    if (first === undefined) return Direction.Idle;
 
    let nearest = first;
    for (const candidate of ordered.slice(1)) {
      const candidateDistance = Math.abs(candidate - floor);
      const nearestDistance = Math.abs(nearest - floor);
      if (
        candidateDistance < nearestDistance ||
        (candidateDistance === nearestDistance && candidate < nearest)
      ) {
        nearest = candidate;
      }
    }
 
    if (nearest > floor) return Direction.Up;
    if (nearest < floor) return Direction.Down;
    return Direction.Idle;
  }
}
 
interface ElevatorState {
  readonly name: string;
  readonly direction: Direction;
  step(car: ElevatorCar): CarStep;
}
 
class ElevatorCar {
  private currentFloor: number;
  private readonly stops = new StopQueue();
  private state: ElevatorState;
 
  public constructor(
    public readonly id: string,
    public readonly floorCount: number,
    initialFloor: number,
  ) {
    if (id.trim().length === 0) {
      throw new Error("car id is required");
    }
    if (!Number.isInteger(floorCount) || floorCount < 2) {
      throw new Error("a building needs at least two floors");
    }
    if (
      !Number.isInteger(initialFloor) ||
      initialFloor < 0 ||
      initialFloor >= floorCount
    ) {
      throw new Error("initial car floor is out of range");
    }
 
    this.currentFloor = initialFloor;
    this.state = new IdleState();
  }
 
  public get floor(): number {
    return this.currentFloor;
  }
 
  public get direction(): Direction {
    return this.state.direction;
  }
 
  public addStop(floor: number): void {
    this.requireFloor(floor);
    this.stops.add(floor);
  }
 
  public step(): CarStep {
    return this.state.step(this);
  }
 
  public report(): CarReport {
    return {
      carId: this.id,
      floor: this.currentFloor,
      direction: this.state.direction,
      state: this.state.name,
      pendingStops: this.stops.ascending(),
    };
  }
 
  public transitionTo(state: ElevatorState): void {
    this.state = state;
  }
 
  public takeStopHere(): boolean {
    return this.stops.take(this.currentFloor);
  }
 
  public hasStopAbove(): boolean {
    return this.stops.hasAbove(this.currentFloor);
  }
 
  public hasStopBelow(): boolean {
    return this.stops.hasBelow(this.currentFloor);
  }
 
  public directionToNearestStop(): Direction {
    return this.stops.directionToNearest(this.currentFloor);
  }
 
  public directionAfter(preferred: Direction): Direction {
    if (preferred === Direction.Up && this.hasStopAbove()) {
      return Direction.Up;
    }
    if (preferred === Direction.Down && this.hasStopBelow()) {
      return Direction.Down;
    }
    if (preferred === Direction.Up && this.hasStopBelow()) {
      return Direction.Down;
    }
    if (preferred === Direction.Down && this.hasStopAbove()) {
      return Direction.Up;
    }
    return this.directionToNearestStop();
  }
 
  public moveOneFloor(direction: Direction.Up | Direction.Down): void {
    const nextFloor =
      this.currentFloor + (direction === Direction.Up ? 1 : -1);
    this.requireFloor(nextFloor);
    this.currentFloor = nextFloor;
  }
 
  public event(kind: CarStepKind): CarStep {
    return {
      carId: this.id,
      floor: this.currentFloor,
      state: this.state.name,
      kind,
    };
  }
 
  private requireFloor(floor: number): void {
    if (
      !Number.isInteger(floor) ||
      floor < 0 ||
      floor >= this.floorCount
    ) {
      throw new Error(`floor ${floor} is out of range for car ${this.id}`);
    }
  }
}
 
class IdleState implements ElevatorState {
  public readonly name = "idle";
  public readonly direction = Direction.Idle;
 
  public step(car: ElevatorCar): CarStep {
    if (car.takeStopHere()) {
      car.transitionTo(new DoorsOpenState(Direction.Idle));
      return car.event("doors-open");
    }
 
    const direction = car.directionToNearestStop();
    if (direction === Direction.Idle) return car.event("idle");
 
    const nextState =
      direction === Direction.Up
        ? new MovingUpState()
        : new MovingDownState();
    car.transitionTo(nextState);
    return nextState.step(car);
  }
}
 
class MovingUpState implements ElevatorState {
  public readonly name = "moving-up";
  public readonly direction = Direction.Up;
 
  public step(car: ElevatorCar): CarStep {
    if (car.takeStopHere()) {
      car.transitionTo(new DoorsOpenState(Direction.Up));
      return car.event("doors-open");
    }
 
    if (!car.hasStopAbove()) {
      if (car.hasStopBelow()) {
        const nextState = new MovingDownState();
        car.transitionTo(nextState);
        return nextState.step(car);
      }
      car.transitionTo(new IdleState());
      return car.event("idle");
    }
 
    car.moveOneFloor(Direction.Up);
    if (car.takeStopHere()) {
      car.transitionTo(new DoorsOpenState(Direction.Up));
      return car.event("doors-open");
    }
    return car.event("moved");
  }
}
 
class MovingDownState implements ElevatorState {
  public readonly name = "moving-down";
  public readonly direction = Direction.Down;
 
  public step(car: ElevatorCar): CarStep {
    if (car.takeStopHere()) {
      car.transitionTo(new DoorsOpenState(Direction.Down));
      return car.event("doors-open");
    }
 
    if (!car.hasStopBelow()) {
      if (car.hasStopAbove()) {
        const nextState = new MovingUpState();
        car.transitionTo(nextState);
        return nextState.step(car);
      }
      car.transitionTo(new IdleState());
      return car.event("idle");
    }
 
    car.moveOneFloor(Direction.Down);
    if (car.takeStopHere()) {
      car.transitionTo(new DoorsOpenState(Direction.Down));
      return car.event("doors-open");
    }
    return car.event("moved");
  }
}
 
class DoorsOpenState implements ElevatorState {
  public readonly name = "doors-open";
 
  public constructor(
    public readonly direction: Direction,
  ) {}
 
  public step(car: ElevatorCar): CarStep {
    const nextDirection = car.directionAfter(this.direction);
 
    if (nextDirection === Direction.Up) {
      car.transitionTo(new MovingUpState());
    } else if (nextDirection === Direction.Down) {
      car.transitionTo(new MovingDownState());
    } else {
      car.transitionTo(new IdleState());
    }
 
    return car.event("doors-closed");
  }
}
 
interface DispatchStrategy {
  selectCar(
    call: HallCall,
    cars: readonly ElevatorCar[],
  ): ElevatorCar;
}
 
class NearestSuitableCarStrategy implements DispatchStrategy {
  public selectCar(
    call: HallCall,
    cars: readonly ElevatorCar[],
  ): ElevatorCar {
    if (cars.length === 0) {
      throw new Error("no elevator cars are available");
    }
 
    const ranked = [...cars].sort((a, b) => {
      const aScore = this.score(call, a);
      const bScore = this.score(call, b);
      return (
        aScore.suitability - bScore.suitability ||
        aScore.distance - bScore.distance ||
        a.id.localeCompare(b.id)
      );
    });
    const selected = ranked[0];
    if (selected === undefined) {
      throw new Error("dispatch could not select a car");
    }
    return selected;
  }
 
  private score(
    call: HallCall,
    car: ElevatorCar,
  ): { suitability: number; distance: number } {
    const idle = car.direction === Direction.Idle;
    const movingTowardCall =
      (car.direction === Direction.Up &&
        call.direction === Direction.Up &&
        car.floor <= call.floor) ||
      (car.direction === Direction.Down &&
        call.direction === Direction.Down &&
        car.floor >= call.floor);
 
    return {
      suitability: idle || movingTowardCall ? 0 : 1,
      distance: Math.abs(car.floor - call.floor),
    };
  }
}
 
interface HallAssignment {
  readonly call: HallCall;
  readonly carId: string;
}
 
class ElevatorSystem {
  private readonly cars: readonly ElevatorCar[];
  private callSequence = 0;
 
  public constructor(
    private readonly floorCount: number,
    cars: readonly ElevatorCar[],
    private readonly dispatch: DispatchStrategy,
  ) {
    if (!Number.isInteger(floorCount) || floorCount < 2) {
      throw new Error("a building needs at least two floors");
    }
    if (cars.length === 0) {
      throw new Error("at least one elevator car is required");
    }
 
    const ids = new Set<string>();
    for (const car of cars) {
      if (ids.has(car.id)) {
        throw new Error(`duplicate elevator car ${car.id}`);
      }
      if (car.floorCount !== floorCount) {
        throw new Error(`car ${car.id} serves a different floor range`);
      }
      ids.add(car.id);
    }
 
    this.cars = [...cars].sort((a, b) => a.id.localeCompare(b.id));
  }
 
  public hallCall(
    floor: number,
    direction: Direction.Up | Direction.Down,
  ): HallAssignment {
    this.requireFloor(floor);
    if (floor === 0 && direction === Direction.Down) {
      throw new Error("the ground floor has no down hall call");
    }
    if (
      floor === this.floorCount - 1 &&
      direction === Direction.Up
    ) {
      throw new Error("the top floor has no up hall call");
    }
 
    const call: HallCall = {
      id: `H-${++this.callSequence}`,
      floor,
      direction,
    };
    const car = this.dispatch.selectCar(call, this.cars);
    car.addStop(floor);
    return { call, carId: car.id };
  }
 
  public requestFloor(carId: string, floor: number): void {
    this.requireFloor(floor);
    const car = this.cars.find((candidate) => candidate.id === carId);
    if (car === undefined) {
      throw new Error(`unknown elevator car ${carId}`);
    }
    car.addStop(floor);
  }
 
  public step(): readonly CarStep[] {
    return this.cars.map((car) => car.step());
  }
 
  public reports(): readonly CarReport[] {
    return this.cars.map((car) => car.report());
  }
 
  private requireFloor(floor: number): void {
    if (
      !Number.isInteger(floor) ||
      floor < 0 ||
      floor >= this.floorCount
    ) {
      throw new Error(`floor ${floor} is outside the building`);
    }
  }
}
 
const system = new ElevatorSystem(
  10,
  [
    new ElevatorCar("A", 10, 0),
    new ElevatorCar("B", 10, 9),
  ],
  new NearestSuitableCarStrategy(),
);
 
const first = system.hallCall(4, Direction.Up);
const second = system.hallCall(7, Direction.Down);
 
console.log(
  `${first.call.id}: car ${first.carId} for floor ${first.call.floor} ${first.call.direction}`,
);
console.log(
  `${second.call.id}: car ${second.carId} for floor ${second.call.floor} ${second.call.direction}`,
);
 
for (let tick = 1; tick <= 2; tick += 1) {
  const events = system.step();
  const status = system.reports()
    .map((car) => `${car.carId}@${car.floor} ${car.state}`)
    .join(" | ");
  console.log(`Tick ${tick}: ${status}`);
 
  const arrival = events.find((event) => event.kind === "doors-open");
  if (arrival !== undefined) {
    console.log(
      `Arrival: car ${arrival.carId} opened at floor ${arrival.floor}`,
    );
  }
}

H-1: car A for floor 4 up H-2: car B for floor 7 down Tick 1: A@1 moving-up | B@8 moving-down Tick 2: A@2 moving-up | B@7 doors-open Arrival: car B opened at floor 7

Both cars start idle, so distance assigns floor 4 to A and floor 7 to B. On each tick, IdleState chooses a motion state and lets it advance one floor. B reaches its requested floor on tick two, removes that stop, and installs DoorsOpenState; A continues its upward sweep.

The sorted set coalesces duplicate floor stops inside one car, but it does not coalesce duplicate hall-call records across dispatch workers. That is a building-level consistency rule and belongs around ElevatorSystem.hallCall() in durable storage.

Design-review checklist

This is where the interviewer moves from a two-car simulation to operating pressure.

  • Why dispatch is Strategy: Car assignment is one replaceable algorithm with a stable selectCar(call, cars) contract. Nearest suitable, zoning, round-robin, and estimated-time policies can change without reopening ElevatorCar.
  • Why movement is State: A car changes its own behavior after events: idle chooses motion, motion reaches a stop, doors open, and the next step closes them. These are lifecycle transitions from state, not client-selected algorithms.
  • Strategy versus State ownership: The system or configuration selects a dispatch strategy. Concrete elevator states select their successors. That collaboration difference matters more than the similar interface-and-implementations diagrams.
  • LOOK versus FCFS: FCFS can reverse a car for every newly inserted request and waste travel. LOOK serves all pending stops ahead before reversing, but continuous same-direction work can delay the other side; add aging or a maximum-wait constraint when fairness is required.
  • SCAN versus LOOK: SCAN goes to the physical boundary; LOOK reverses at the furthest outstanding stop. Name the chosen variant because “directional queue” alone does not define its reversal point.
  • Hall-call direction: UP and DOWN are distinct demand signals, including separate duplicate keys. The baseline uses them for suitability; a stronger queue retains desired pickup direction and opens only on the matching sweep.
  • Internal requests: A destination selected inside car B must stay on B. Sending it back through group dispatch could move a boarded rider's request to another car.
  • Idle-car parking: Staying at the last floor is cheap and deterministic. Lobby parking helps morning arrivals but can hurt upper floors and energy use; distributed parking or learned demand belongs in a policy with measured goals.
  • Dispatch scoring: Nearest distance alone may choose a car moving away. The baseline first ranks suitability, then distance, then car ID for deterministic ties. Estimated time to destination would include queued stops and door dwell rather than raw distance.
  • Multiple assignment race: Two workers can select different cars for the same pending (floor, direction) call. Atomically change the call from pending to assigned with a version or conditional update, then append the winning car's stop in the same workflow.
  • Calls added during movement: Serialize commands per car or protect a versioned request queue. A lost update between step() and addStop() can drop a rider even when dispatch itself is correct.
  • Arrival and door failures: The model marks service at DoorsOpenState. Real sensors can report obstruction, motor failure, or uncertain door state; add explicit fault states and idempotent commands instead of pretending a timer proves physical arrival.
  • Starvation and service goals: LOOK gives an order, not a wait-time guarantee. Track call age, cap deferral, and measure percentile wait and journey time before claiming efficiency.
  • Testing dispatch: Cover idle and same-direction suitability, calls ahead and behind, deterministic ties, no cars, top and ground boundary calls, and an all-cars-unsuitable fallback.
  • Testing movement: Add stops above and below, assert ascending service before reversal and descending service afterward, verify one door-open event per stop, and add requests during both sweeps. No accepted stop may disappear without an arrival.
  • Testing concurrency: Race duplicate hall calls against the real persistence boundary and assert one assignment. Race queue append with a movement tick and assert the new stop remains either pending or served, never lost.
  • With more time: Add pending-call records, estimated-time dispatch, direction-aware pickup queues, aging, idle repositioning, telemetry, fault states, and hardware ports only after defining their timing and consistency contracts.

Checkpoint

Answer all three to mark this lesson complete

The elevator system gives group assignment and car movement separate homes, then makes each limitation visible. The remaining case studies—booking with seat locks, an LRU cache, a rate limiter, and chess—push on concurrency and algorithmic design.

+50 XP on completion