Designing an Elevator System

Difficulty: Intermediate Patterns: Strategy, State, Singleton, Command Asked at: Amazon, Uber, PhonePe, Google, Flipkart


The elevator problem looks like it’s about physics but it’s really about scheduling: given a set of requests, which car goes where, and in what order does a car serve the floors it’s committed to? A strong answer separates three concerns — the car (moves and tracks state), the dispatcher (picks which car handles a request), and the movement strategy (what order a car serves its stops). Bundle those together and you get a 400-line method; keep them apart and each is small and swappable.


Functional Requirements

  1. A building has N floors and M elevator cars.
  2. Two kinds of request:
    • External (hall call): pressed on a floor, has a direction (up/down).
    • Internal (car call): pressed inside a car, has a target floor.
  3. A dispatcher assigns each external request to the most suitable car.
  4. Each car serves its committed stops in an efficient order (SCAN / LOOK — keep going one way, then reverse).
  5. A car tracks its direction: UP, DOWN, or IDLE.
  6. Doors open on arrival at a committed floor.

Non-Functional Requirements

  1. No starvation — a request is eventually served, not indefinitely skipped.
  2. Extensibility — new dispatch policy or movement strategy = one new class.
  3. Concurrency-ready — requests can arrive while cars are moving.

Core Entities

Entity Description
Direction Enum: UP, DOWN, IDLE
Request A floor to serve, with an optional direction (hall calls)
ElevatorCar One physical car: current floor, direction, and its set of pending stops
MovementStrategy Given a car’s stops, decide the next floor (SCAN/LOOK today)
DispatchStrategy Given all cars + a request, pick the car to serve it
ElevatorSystem Facade: holds cars, routes requests, ticks the simulation

Two strategies, two decisions

The design hinges on splitting the two independent choices:

💡 Strategy pattern = encapsulate each algorithm behind a common interface so the context can swap it at runtime. Two orthogonal decisions → two strategy interfaces, composed independently.

classDiagram
    class Direction {
        <<enumeration>>
        UP
        DOWN
        IDLE
    }

    class ElevatorCar {
        -int id
        -int currentFloor
        -Direction direction
        -TreeSet~Integer~ upStops
        -TreeSet~Integer~ downStops
        +addStop(int floor)
        +step(MovementStrategy)
        +distanceTo(int floor) int
    }

    class MovementStrategy {
        <<interface>>
        +nextFloor(ElevatorCar car) Integer
    }

    class LookStrategy {
        +nextFloor(ElevatorCar) Integer
    }

    class DispatchStrategy {
        <<interface>>
        +selectCar(List~ElevatorCar~ cars, Request req) ElevatorCar
    }

    class NearestCarDispatch {
        +selectCar(List, Request) ElevatorCar
    }

    class ElevatorSystem {
        -List~ElevatorCar~ cars
        -DispatchStrategy dispatch
        -MovementStrategy movement
        +requestHallCall(int floor, Direction dir)
        +requestCarCall(int carId, int floor)
        +step()
    }

    MovementStrategy <|.. LookStrategy
    DispatchStrategy <|.. NearestCarDispatch
    ElevatorSystem --> ElevatorCar
    ElevatorSystem --> DispatchStrategy
    ElevatorSystem --> MovementStrategy
    ElevatorCar ..> MovementStrategy

Design Patterns

Pattern Where Why
Strategy DispatchStrategy + MovementStrategy Two orthogonal, swappable decisions. Zone dispatch or SCAN vs LOOK = one class each.
State Direction drives which stop-set a car serves UP serves the up-set ascending, DOWN serves the down-set descending, IDLE picks either.
Facade ElevatorSystem One entry point (requestHallCall, step) hides car/dispatch/movement wiring.
Command (extension) Request objects queued and replayed Enables logging, replay, and prioritisation.

Data Structures

Component Structure Why
A car’s pending stops Two TreeSet<Integer>upStops, downStops Sorted; LOOK reads the next stop above/below currentFloor in O(log n)
Cars ArrayList<ElevatorCar> Dispatcher scans all cars — small M, linear is fine
Direction enum Drives which set is active and prevents illegal moves

Why two sorted sets rather than one queue? Because LOOK needs “the nearest committed floor above me” and “the nearest below me” cheaply. A TreeSet gives both via ceiling() / floor() in log time, and dedupes repeated presses for free.


Complete Code

Direction.java

package elevator.model;

public enum Direction {
    UP, DOWN, IDLE
}
from enum import Enum


class Direction(Enum):
    UP = "UP"
    DOWN = "DOWN"
    IDLE = "IDLE"
#pragma once

enum class Direction {
    UP,
    DOWN,
    IDLE
};
class Direction {
    static UP = "UP";
    static DOWN = "DOWN";
    static IDLE = "IDLE";
}

export default Direction;

Request.java

A request is a floor plus an optional direction. Hall calls carry a direction (you press “up” or “down” in the lobby); car calls just carry a target floor.

package elevator.model;

public class Request {
    private final int floor;
    private final Direction direction; // null for internal car calls

    public Request(int floor, Direction direction) {
        this.floor = floor;
        this.direction = direction;
    }

    public int getFloor() { return floor; }
    public Direction getDirection() { return direction; }

    @Override
    public String toString() {
        return "Request[floor=" + floor + (direction != null ? ", dir=" + direction : "") + "]";
    }
}
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional

from elevator.model.direction import Direction


@dataclass(frozen=True)
class Request:
    floor: int
    direction: Optional[Direction] = None  # None for internal car calls

    def __str__(self) -> str:
        dir_part = f", dir={self.direction.value}" if self.direction else ""
        return f"Request[floor={self.floor}{dir_part}]"
#pragma once
#include <string>
#include <optional>
#include "Direction.h"

class Request {
    int floor_;
    std::optional<Direction> direction_; // nullopt for internal car calls

public:
    Request(int floor, std::optional<Direction> direction)
        : floor_(floor), direction_(direction) {}

    int getFloor() const { return floor_; }
    std::optional<Direction> getDirection() const { return direction_; }

    std::string toString() const {
        std::string s = "Request[floor=" + std::to_string(floor_);
        if (direction_.has_value()) {
            s += ", dir=" + std::to_string(static_cast<int>(direction_.value()));
        }
        return s + "]";
    }
};
import Direction from "./Direction.js";

class Request {
    #floor;
    #direction; // null for internal car calls

    constructor(floor, direction = null) {
        this.#floor = floor;
        this.#direction = direction;
    }

    getFloor() { return this.#floor; }
    getDirection() { return this.#direction; }

    toString() {
        const dirPart = this.#direction ? `, dir=${this.#direction}` : "";
        return `Request[floor=${this.#floor}${dirPart}]`;
    }
}

export default Request;

ElevatorCar.java

The car owns its position, direction, and its two sorted stop-sets. step() delegates the “where next” decision to a MovementStrategy, then moves one floor toward it — so the car doesn’t hardcode SCAN vs LOOK.

package elevator.model;

import elevator.movement.MovementStrategy;

import java.util.TreeSet;

public class ElevatorCar {
    private final int id;
    private int currentFloor;
    private Direction direction = Direction.IDLE;

    // Committed stops, split by travel direction for LOOK scheduling.
    private final TreeSet<Integer> upStops = new TreeSet<>();
    private final TreeSet<Integer> downStops = new TreeSet<>();

    public ElevatorCar(int id, int startFloor) {
        this.id = id;
        this.currentFloor = startFloor;
    }

    /** Commit to serving a floor. Bucketed by where it sits relative to us. */
    public void addStop(int floor) {
        if (floor == currentFloor) { openDoors(); return; }
        if (floor > currentFloor) upStops.add(floor);
        else downStops.add(floor);
        if (direction == Direction.IDLE) {
            direction = floor > currentFloor ? Direction.UP : Direction.DOWN;
        }
    }

    /** Advance one floor toward the strategy's chosen target. */
    public void step(MovementStrategy strategy) {
        Integer target = strategy.nextFloor(this);
        if (target == null) { direction = Direction.IDLE; return; }

        if (target > currentFloor) { currentFloor++; direction = Direction.UP; }
        else if (target < currentFloor) { currentFloor--; direction = Direction.DOWN; }

        if (currentFloor == target) {
            upStops.remove(target);
            downStops.remove(target);
            openDoors();
        }
    }

    private void openDoors() {
        System.out.println("  🚪 Car " + id + " opens doors at floor " + currentFloor);
    }

    /** Cost function the dispatcher uses. Cheap: pure distance for now. */
    public int distanceTo(int floor) { return Math.abs(currentFloor - floor); }

    public int getId() { return id; }
    public int getCurrentFloor() { return currentFloor; }
    public Direction getDirection() { return direction; }
    public TreeSet<Integer> getUpStops() { return upStops; }
    public TreeSet<Integer> getDownStops() { return downStops; }
    public boolean isIdle() { return upStops.isEmpty() && downStops.isEmpty(); }

    @Override
    public String toString() {
        return "Car " + id + " @floor " + currentFloor + " (" + direction + ") stops↑" + upStops + " ↓" + downStops;
    }
}
from __future__ import annotations
from sortedcontainers import SortedList
from typing import Optional, TYPE_CHECKING

from elevator.model.direction import Direction

if TYPE_CHECKING:
    from elevator.movement.movement_strategy import MovementStrategy


class ElevatorCar:
    def __init__(self, car_id: int, start_floor: int) -> None:
        self._id = car_id
        self._current_floor = start_floor
        self._direction = Direction.IDLE
        # Committed stops, split by travel direction for LOOK scheduling.
        self._up_stops: SortedList[int] = SortedList()
        self._down_stops: SortedList[int] = SortedList()

    def add_stop(self, floor: int) -> None:
        """Commit to serving a floor. Bucketed by where it sits relative to us."""
        if floor == self._current_floor:
            self._open_doors()
            return
        if floor > self._current_floor:
            self._up_stops.add(floor)
        else:
            self._down_stops.add(floor)
        if self._direction == Direction.IDLE:
            self._direction = Direction.UP if floor > self._current_floor else Direction.DOWN

    def step(self, strategy: MovementStrategy) -> None:
        """Advance one floor toward the strategy's chosen target."""
        target: Optional[int] = strategy.next_floor(self)
        if target is None:
            self._direction = Direction.IDLE
            return

        if target > self._current_floor:
            self._current_floor += 1
            self._direction = Direction.UP
        elif target < self._current_floor:
            self._current_floor -= 1
            self._direction = Direction.DOWN

        if self._current_floor == target:
            self._up_stops.discard(target)
            self._down_stops.discard(target)
            self._open_doors()

    def _open_doors(self) -> None:
        print(f"  🚪 Car {self._id} opens doors at floor {self._current_floor}")

    def distance_to(self, floor: int) -> int:
        """Cost function the dispatcher uses."""
        return abs(self._current_floor - floor)

    @property
    def id(self) -> int:
        return self._id

    @property
    def current_floor(self) -> int:
        return self._current_floor

    @property
    def direction(self) -> Direction:
        return self._direction

    @property
    def up_stops(self) -> SortedList[int]:
        return self._up_stops

    @property
    def down_stops(self) -> SortedList[int]:
        return self._down_stops

    @property
    def is_idle(self) -> bool:
        return len(self._up_stops) == 0 and len(self._down_stops) == 0

    def __str__(self) -> str:
        return (f"Car {self._id} @floor {self._current_floor} "
                f"({self._direction.value}) stops↑{list(self._up_stops)} ↓{list(self._down_stops)}")
#pragma once
#include <set>
#include <string>
#include <iostream>
#include <cmath>
#include <optional>
#include "Direction.h"

// Forward declaration
class MovementStrategy;

class ElevatorCar {
    int id_;
    int currentFloor_;
    Direction direction_ = Direction::IDLE;
    std::set<int> upStops_;
    std::set<int> downStops_;

    void openDoors() {
        std::cout << "  🚪 Car " << id_ << " opens doors at floor " << currentFloor_ << "\n";
    }

public:
    ElevatorCar(int id, int startFloor) : id_(id), currentFloor_(startFloor) {}

    void addStop(int floor) {
        if (floor == currentFloor_) { openDoors(); return; }
        if (floor > currentFloor_) upStops_.insert(floor);
        else downStops_.insert(floor);
        if (direction_ == Direction::IDLE) {
            direction_ = floor > currentFloor_ ? Direction::UP : Direction::DOWN;
        }
    }

    void step(MovementStrategy& strategy);

    int distanceTo(int floor) const { return std::abs(currentFloor_ - floor); }

    int getId() const { return id_; }
    int getCurrentFloor() const { return currentFloor_; }
    Direction getDirection() const { return direction_; }
    const std::set<int>& getUpStops() const { return upStops_; }
    const std::set<int>& getDownStops() const { return downStops_; }
    std::set<int>& getUpStopsMut() { return upStops_; }
    std::set<int>& getDownStopsMut() { return downStops_; }
    bool isIdle() const { return upStops_.empty() && downStops_.empty(); }

    void setDirection(Direction d) { direction_ = d; }
    void setCurrentFloor(int f) { currentFloor_ = f; }

    std::string toString() const {
        std::string s = "Car " + std::to_string(id_) + " @floor " + std::to_string(currentFloor_);
        s += " (" + std::to_string(static_cast<int>(direction_)) + ") stops↑[";
        for (auto it = upStops_.begin(); it != upStops_.end(); ++it) {
            if (it != upStops_.begin()) s += ", ";
            s += std::to_string(*it);
        }
        s += "] ↓[";
        for (auto it = downStops_.begin(); it != downStops_.end(); ++it) {
            if (it != downStops_.begin()) s += ", ";
            s += std::to_string(*it);
        }
        return s + "]";
    }
};
import Direction from "./Direction.js";

class ElevatorCar {
    #id;
    #currentFloor;
    #direction = Direction.IDLE;
    #upStops = new Set();   // Sorted via manual iteration
    #downStops = new Set();

    constructor(id, startFloor) {
        this.#id = id;
        this.#currentFloor = startFloor;
    }

    /** Commit to serving a floor. Bucketed by where it sits relative to us. */
    addStop(floor) {
        if (floor === this.#currentFloor) { this.#openDoors(); return; }
        if (floor > this.#currentFloor) this.#upStops.add(floor);
        else this.#downStops.add(floor);
        if (this.#direction === Direction.IDLE) {
            this.#direction = floor > this.#currentFloor ? Direction.UP : Direction.DOWN;
        }
    }

    /** Advance one floor toward the strategy's chosen target. */
    step(strategy) {
        const target = strategy.nextFloor(this);
        if (target === null) { this.#direction = Direction.IDLE; return; }

        if (target > this.#currentFloor) { this.#currentFloor++; this.#direction = Direction.UP; }
        else if (target < this.#currentFloor) { this.#currentFloor--; this.#direction = Direction.DOWN; }

        if (this.#currentFloor === target) {
            this.#upStops.delete(target);
            this.#downStops.delete(target);
            this.#openDoors();
        }
    }

    #openDoors() {
        console.log(`  🚪 Car ${this.#id} opens doors at floor ${this.#currentFloor}`);
    }

    distanceTo(floor) { return Math.abs(this.#currentFloor - floor); }

    getId() { return this.#id; }
    getCurrentFloor() { return this.#currentFloor; }
    getDirection() { return this.#direction; }
    getUpStops() { return [...this.#upStops].sort((a, b) => a - b); }
    getDownStops() { return [...this.#downStops].sort((a, b) => a - b); }
    isIdle() { return this.#upStops.size === 0 && this.#downStops.size === 0; }

    toString() {
        return `Car ${this.#id} @floor ${this.#currentFloor} (${this.#direction}) ` +
            `stops↑[${this.getUpStops()}] ↓[${this.getDownStops()}]`;
    }
}

export default ElevatorCar;

MovementStrategy.java (Strategy interface)

package elevator.movement;

import elevator.model.ElevatorCar;

public interface MovementStrategy {
    /** @return the floor the car should head toward next, or null if it has none. */
    Integer nextFloor(ElevatorCar car);
}
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Optional, TYPE_CHECKING

if TYPE_CHECKING:
    from elevator.model.elevator_car import ElevatorCar


class MovementStrategy(ABC):
    @abstractmethod
    def next_floor(self, car: ElevatorCar) -> Optional[int]:
        """Return the floor the car should head toward next, or None if it has none."""
        ...
#pragma once
#include <optional>

class ElevatorCar;

class MovementStrategy {
public:
    virtual ~MovementStrategy() = default;
    /** @return the floor the car should head toward next, or nullopt if it has none. */
    virtual std::optional<int> nextFloor(ElevatorCar& car) = 0;
};
/**
 * @interface MovementStrategy
 * @method nextFloor(car: ElevatorCar): number | null
 */
class MovementStrategy {
    /** @return the floor the car should head toward next, or null if it has none. */
    nextFloor(car) {
        throw new Error("nextFloor() must be implemented");
    }
}

export default MovementStrategy;

LookStrategy.java

The LOOK algorithm: keep serving stops in the current direction until none remain ahead, then reverse. It reads the nearest committed floor above (ceiling) or below (floor) the car in O(log n) from the sorted sets — this is why the car stores two TreeSets.

package elevator.movement;

import elevator.model.Direction;
import elevator.model.ElevatorCar;

public class LookStrategy implements MovementStrategy {
    @Override
    public Integer nextFloor(ElevatorCar car) {
        int floor = car.getCurrentFloor();
        Direction dir = car.getDirection();

        if (dir == Direction.UP) {
            Integer up = car.getUpStops().ceiling(floor);
            if (up != null) return up;
            // Nothing more above: reverse and serve the highest pending below.
            return car.getDownStops().isEmpty() ? null : car.getDownStops().last();
        }
        if (dir == Direction.DOWN) {
            Integer down = car.getDownStops().floor(floor);
            if (down != null) return down;
            return car.getUpStops().isEmpty() ? null : car.getUpStops().first();
        }
        // IDLE: pick whichever set has work, nearest first.
        if (!car.getUpStops().isEmpty()) return car.getUpStops().first();
        if (!car.getDownStops().isEmpty()) return car.getDownStops().last();
        return null;
    }
}
from __future__ import annotations
from typing import Optional, TYPE_CHECKING

from elevator.model.direction import Direction
from elevator.movement.movement_strategy import MovementStrategy

if TYPE_CHECKING:
    from elevator.model.elevator_car import ElevatorCar


class LookStrategy(MovementStrategy):
    def next_floor(self, car: ElevatorCar) -> Optional[int]:
        floor = car.current_floor
        direction = car.direction

        if direction == Direction.UP:
            # Find nearest stop at or above current floor
            idx = car.up_stops.bisect_left(floor)
            if idx < len(car.up_stops):
                return car.up_stops[idx]
            # Nothing more above: reverse and serve the highest pending below.
            return car.down_stops[-1] if car.down_stops else None

        if direction == Direction.DOWN:
            # Find nearest stop at or below current floor
            idx = car.down_stops.bisect_right(floor) - 1
            if idx >= 0:
                return car.down_stops[idx]
            return car.up_stops[0] if car.up_stops else None

        # IDLE: pick whichever set has work, nearest first.
        if car.up_stops:
            return car.up_stops[0]
        if car.down_stops:
            return car.down_stops[-1]
        return None
#pragma once
#include "MovementStrategy.h"
#include "ElevatorCar.h"
#include "Direction.h"

class LookStrategy : public MovementStrategy {
public:
    std::optional<int> nextFloor(ElevatorCar& car) override {
        int floor = car.getCurrentFloor();
        Direction dir = car.getDirection();

        if (dir == Direction::UP) {
            // ceiling: first element >= floor
            auto it = car.getUpStops().lower_bound(floor);
            if (it != car.getUpStops().end()) return *it;
            // Nothing more above: reverse.
            if (car.getDownStops().empty()) return std::nullopt;
            return *car.getDownStops().rbegin();
        }
        if (dir == Direction::DOWN) {
            // floor: last element <= floor
            auto it = car.getDownStops().upper_bound(floor);
            if (it != car.getDownStops().begin()) { --it; return *it; }
            if (car.getUpStops().empty()) return std::nullopt;
            return *car.getUpStops().begin();
        }
        // IDLE: pick whichever set has work, nearest first.
        if (!car.getUpStops().empty()) return *car.getUpStops().begin();
        if (!car.getDownStops().empty()) return *car.getDownStops().rbegin();
        return std::nullopt;
    }
};
import Direction from "../model/Direction.js";
import MovementStrategy from "./MovementStrategy.js";

class LookStrategy extends MovementStrategy {
    nextFloor(car) {
        const floor = car.getCurrentFloor();
        const dir = car.getDirection();
        const upStops = car.getUpStops();   // sorted ascending
        const downStops = car.getDownStops(); // sorted ascending

        if (dir === Direction.UP) {
            const up = upStops.find(s => s >= floor);
            if (up !== undefined) return up;
            // Nothing more above: reverse.
            return downStops.length > 0 ? downStops[downStops.length - 1] : null;
        }
        if (dir === Direction.DOWN) {
            const down = [...downStops].reverse().find(s => s <= floor);
            if (down !== undefined) return down;
            return upStops.length > 0 ? upStops[0] : null;
        }
        // IDLE: pick whichever set has work, nearest first.
        if (upStops.length > 0) return upStops[0];
        if (downStops.length > 0) return downStops[downStops.length - 1];
        return null;
    }
}

export default LookStrategy;

DispatchStrategy.java (Strategy interface)

package elevator.dispatch;

import elevator.model.ElevatorCar;
import elevator.model.Request;

import java.util.List;

public interface DispatchStrategy {
    ElevatorCar selectCar(List<ElevatorCar> cars, Request request);
}
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List, TYPE_CHECKING

if TYPE_CHECKING:
    from elevator.model.elevator_car import ElevatorCar
    from elevator.model.request import Request


class DispatchStrategy(ABC):
    @abstractmethod
    def select_car(self, cars: List[ElevatorCar], request: Request) -> ElevatorCar:
        """Pick the best car to serve the given request."""
        ...
#pragma once
#include <vector>
#include "ElevatorCar.h"
#include "Request.h"

class DispatchStrategy {
public:
    virtual ~DispatchStrategy() = default;
    virtual ElevatorCar& selectCar(std::vector<ElevatorCar>& cars, const Request& request) = 0;
};
/**
 * @interface DispatchStrategy
 * @method selectCar(cars: ElevatorCar[], request: Request): ElevatorCar
 */
class DispatchStrategy {
    selectCar(cars, request) {
        throw new Error("selectCar() must be implemented");
    }
}

export default DispatchStrategy;

NearestCarDispatch.java

Picks the car with the smallest cost to reach the request floor, gently preferring idle cars so moving cars aren’t overloaded. This is the swappable policy — a zone-based or least-loaded dispatcher slots in here without touching the system.

package elevator.dispatch;

import elevator.model.ElevatorCar;
import elevator.model.Request;

import java.util.List;

public class NearestCarDispatch implements DispatchStrategy {
    @Override
    public ElevatorCar selectCar(List<ElevatorCar> cars, Request request) {
        ElevatorCar best = null;
        int bestCost = Integer.MAX_VALUE;

        for (ElevatorCar car : cars) {
            int cost = car.distanceTo(request.getFloor());
            // Nudge idle cars to win ties so busy cars don't get piled on.
            if (!car.isIdle()) cost += 1;
            if (cost < bestCost) { bestCost = cost; best = car; }
        }
        return best;
    }
}
from __future__ import annotations
from typing import List, TYPE_CHECKING

from elevator.dispatch.dispatch_strategy import DispatchStrategy

if TYPE_CHECKING:
    from elevator.model.elevator_car import ElevatorCar
    from elevator.model.request import Request


class NearestCarDispatch(DispatchStrategy):
    def select_car(self, cars: List[ElevatorCar], request: Request) -> ElevatorCar:
        best: ElevatorCar | None = None
        best_cost = float("inf")

        for car in cars:
            cost = car.distance_to(request.floor)
            # Nudge idle cars to win ties so busy cars don't get piled on.
            if not car.is_idle:
                cost += 1
            if cost < best_cost:
                best_cost = cost
                best = car

        assert best is not None
        return best
#pragma once
#include <vector>
#include <limits>
#include "DispatchStrategy.h"

class NearestCarDispatch : public DispatchStrategy {
public:
    ElevatorCar& selectCar(std::vector<ElevatorCar>& cars, const Request& request) override {
        ElevatorCar* best = nullptr;
        int bestCost = std::numeric_limits<int>::max();

        for (auto& car : cars) {
            int cost = car.distanceTo(request.getFloor());
            // Nudge idle cars to win ties so busy cars don't get piled on.
            if (!car.isIdle()) cost += 1;
            if (cost < bestCost) { bestCost = cost; best = &car; }
        }
        return *best;
    }
};
import DispatchStrategy from "./DispatchStrategy.js";

class NearestCarDispatch extends DispatchStrategy {
    selectCar(cars, request) {
        let best = null;
        let bestCost = Infinity;

        for (const car of cars) {
            let cost = car.distanceTo(request.getFloor());
            // Nudge idle cars to win ties so busy cars don't get piled on.
            if (!car.isIdle()) cost += 1;
            if (cost < bestCost) { bestCost = cost; best = car; }
        }
        return best;
    }
}

export default NearestCarDispatch;

ElevatorSystem.java (Facade)

The single entry point. Hall calls go through the dispatcher to pick a car; car calls target a specific car directly. step() advances every car one floor — call it in a loop to run the simulation.

package elevator;

import elevator.dispatch.DispatchStrategy;
import elevator.dispatch.NearestCarDispatch;
import elevator.model.Direction;
import elevator.model.ElevatorCar;
import elevator.model.Request;
import elevator.movement.LookStrategy;
import elevator.movement.MovementStrategy;

import java.util.ArrayList;
import java.util.List;

public class ElevatorSystem {
    private final List<ElevatorCar> cars = new ArrayList<>();
    private final DispatchStrategy dispatch;
    private final MovementStrategy movement;

    public ElevatorSystem(int carCount, DispatchStrategy dispatch, MovementStrategy movement) {
        this.dispatch = dispatch;
        this.movement = movement;
        for (int i = 1; i <= carCount; i++) cars.add(new ElevatorCar(i, 0));
    }

    /** Convenience: sensible defaults (nearest-car + LOOK). */
    public ElevatorSystem(int carCount) {
        this(carCount, new NearestCarDispatch(), new LookStrategy());
    }

    /** Hall call: someone on `floor` wants to go `dir`. Dispatcher picks the car. */
    public void requestHallCall(int floor, Direction dir) {
        Request req = new Request(floor, dir);
        ElevatorCar car = dispatch.selectCar(cars, req);
        System.out.println("↳ Hall call " + req + " assigned to Car " + car.getId());
        car.addStop(floor);
    }

    /** Car call: passenger inside `carId` presses `floor`. */
    public void requestCarCall(int carId, int floor) {
        ElevatorCar car = cars.get(carId - 1);
        System.out.println("↳ Car call: Car " + carId + " → floor " + floor);
        car.addStop(floor);
    }

    /** Advance the whole system by one tick. */
    public void step() {
        for (ElevatorCar car : cars) {
            if (!car.isIdle()) car.step(movement);
        }
    }

    public boolean allIdle() {
        return cars.stream().allMatch(ElevatorCar::isIdle);
    }

    public void printState() {
        cars.forEach(c -> System.out.println("  " + c));
    }
}
from __future__ import annotations
from typing import List, Optional

from elevator.dispatch.dispatch_strategy import DispatchStrategy
from elevator.dispatch.nearest_car_dispatch import NearestCarDispatch
from elevator.model.direction import Direction
from elevator.model.elevator_car import ElevatorCar
from elevator.model.request import Request
from elevator.movement.look_strategy import LookStrategy
from elevator.movement.movement_strategy import MovementStrategy


class ElevatorSystem:
    def __init__(
        self,
        car_count: int,
        dispatch: Optional[DispatchStrategy] = None,
        movement: Optional[MovementStrategy] = None,
    ) -> None:
        self._dispatch = dispatch or NearestCarDispatch()
        self._movement = movement or LookStrategy()
        self._cars: List[ElevatorCar] = [
            ElevatorCar(i, 0) for i in range(1, car_count + 1)
        ]

    def request_hall_call(self, floor: int, direction: Direction) -> None:
        """Hall call: someone on `floor` wants to go `direction`. Dispatcher picks the car."""
        req = Request(floor, direction)
        car = self._dispatch.select_car(self._cars, req)
        print(f"↳ Hall call {req} assigned to Car {car.id}")
        car.add_stop(floor)

    def request_car_call(self, car_id: int, floor: int) -> None:
        """Car call: passenger inside `car_id` presses `floor`."""
        car = self._cars[car_id - 1]
        print(f"↳ Car call: Car {car_id} → floor {floor}")
        car.add_stop(floor)

    def step(self) -> None:
        """Advance the whole system by one tick."""
        for car in self._cars:
            if not car.is_idle:
                car.step(self._movement)

    def all_idle(self) -> bool:
        return all(car.is_idle for car in self._cars)

    def print_state(self) -> None:
        for car in self._cars:
            print(f"  {car}")
#pragma once
#include <vector>
#include <memory>
#include <iostream>
#include <algorithm>
#include "ElevatorCar.h"
#include "Request.h"
#include "Direction.h"
#include "DispatchStrategy.h"
#include "MovementStrategy.h"
#include "NearestCarDispatch.h"
#include "LookStrategy.h"

class ElevatorSystem {
    std::vector<ElevatorCar> cars_;
    std::unique_ptr<DispatchStrategy> dispatch_;
    std::unique_ptr<MovementStrategy> movement_;

public:
    ElevatorSystem(int carCount,
                   std::unique_ptr<DispatchStrategy> dispatch = std::make_unique<NearestCarDispatch>(),
                   std::unique_ptr<MovementStrategy> movement = std::make_unique<LookStrategy>())
        : dispatch_(std::move(dispatch)), movement_(std::move(movement)) {
        for (int i = 1; i <= carCount; ++i) cars_.emplace_back(i, 0);
    }

    void requestHallCall(int floor, Direction dir) {
        Request req(floor, dir);
        auto& car = dispatch_->selectCar(cars_, req);
        std::cout << "↳ Hall call " << req.toString() << " assigned to Car " << car.getId() << "\n";
        car.addStop(floor);
    }

    void requestCarCall(int carId, int floor) {
        auto& car = cars_[carId - 1];
        std::cout << "↳ Car call: Car " << carId << " → floor " << floor << "\n";
        car.addStop(floor);
    }

    void step() {
        for (auto& car : cars_) {
            if (!car.isIdle()) car.step(*movement_);
        }
    }

    bool allIdle() const {
        return std::all_of(cars_.begin(), cars_.end(),
                           [](const ElevatorCar& c) { return c.isIdle(); });
    }

    void printState() const {
        for (const auto& car : cars_) std::cout << "  " << car.toString() << "\n";
    }
};
import Direction from "./model/Direction.js";
import ElevatorCar from "./model/ElevatorCar.js";
import Request from "./model/Request.js";
import NearestCarDispatch from "./dispatch/NearestCarDispatch.js";
import LookStrategy from "./movement/LookStrategy.js";

class ElevatorSystem {
    #cars = [];
    #dispatch;
    #movement;

    constructor(carCount, dispatch = new NearestCarDispatch(), movement = new LookStrategy()) {
        this.#dispatch = dispatch;
        this.#movement = movement;
        for (let i = 1; i <= carCount; i++) this.#cars.push(new ElevatorCar(i, 0));
    }

    /** Hall call: someone on `floor` wants to go `dir`. Dispatcher picks the car. */
    requestHallCall(floor, dir) {
        const req = new Request(floor, dir);
        const car = this.#dispatch.selectCar(this.#cars, req);
        console.log(`↳ Hall call ${req.toString()} assigned to Car ${car.getId()}`);
        car.addStop(floor);
    }

    /** Car call: passenger inside `carId` presses `floor`. */
    requestCarCall(carId, floor) {
        const car = this.#cars[carId - 1];
        console.log(`↳ Car call: Car ${carId} → floor ${floor}`);
        car.addStop(floor);
    }

    /** Advance the whole system by one tick. */
    step() {
        for (const car of this.#cars) {
            if (!car.isIdle()) car.step(this.#movement);
        }
    }

    allIdle() {
        return this.#cars.every(car => car.isIdle());
    }

    printState() {
        this.#cars.forEach(c => console.log(`  ${c.toString()}`));
    }
}

export default ElevatorSystem;

Demo.java (Runnable end-to-end)

Sets up 2 cars in a 10-floor building, fires a mix of hall and car calls, then ticks the simulation until every car is idle — printing each car’s position so you can watch LOOK serve stops in order and reverse.

package elevator;

import elevator.model.Direction;

public class Demo {
    public static void main(String[] args) {
        ElevatorSystem system = new ElevatorSystem(2); // nearest-car + LOOK

        System.out.println("=== Requests coming in ===");
        system.requestHallCall(5, Direction.UP);   // someone at floor 5 going up
        system.requestHallCall(2, Direction.UP);   // someone at floor 2 going up
        system.requestCarCall(1, 8);               // passenger in car 1 wants floor 8
        system.requestHallCall(9, Direction.DOWN); // someone at floor 9 going down

        System.out.println("\nInitial state:");
        system.printState();

        System.out.println("\n=== Simulation ===");
        int tick = 0;
        while (!system.allIdle() && tick < 30) {
            tick++;
            System.out.println("\n[tick " + tick + "]");
            system.step();
            system.printState();
        }

        System.out.println("\n=== All requests served in " + tick + " ticks ===");
    }
}
from elevator.elevator_system import ElevatorSystem
from elevator.model.direction import Direction


def main() -> None:
    system = ElevatorSystem(2)  # nearest-car + LOOK

    print("=== Requests coming in ===")
    system.request_hall_call(5, Direction.UP)    # someone at floor 5 going up
    system.request_hall_call(2, Direction.UP)    # someone at floor 2 going up
    system.request_car_call(1, 8)               # passenger in car 1 wants floor 8
    system.request_hall_call(9, Direction.DOWN)  # someone at floor 9 going down

    print("\nInitial state:")
    system.print_state()

    print("\n=== Simulation ===")
    tick = 0
    while not system.all_idle() and tick < 30:
        tick += 1
        print(f"\n[tick {tick}]")
        system.step()
        system.print_state()

    print(f"\n=== All requests served in {tick} ticks ===")


if __name__ == "__main__":
    main()
#include <iostream>
#include "ElevatorSystem.h"
#include "Direction.h"

int main() {
    ElevatorSystem system(2); // nearest-car + LOOK

    std::cout << "=== Requests coming in ===\n";
    system.requestHallCall(5, Direction::UP);   // someone at floor 5 going up
    system.requestHallCall(2, Direction::UP);   // someone at floor 2 going up
    system.requestCarCall(1, 8);               // passenger in car 1 wants floor 8
    system.requestHallCall(9, Direction::DOWN); // someone at floor 9 going down

    std::cout << "\nInitial state:\n";
    system.printState();

    std::cout << "\n=== Simulation ===\n";
    int tick = 0;
    while (!system.allIdle() && tick < 30) {
        tick++;
        std::cout << "\n[tick " << tick << "]\n";
        system.step();
        system.printState();
    }

    std::cout << "\n=== All requests served in " << tick << " ticks ===\n";
    return 0;
}
import ElevatorSystem from "./ElevatorSystem.js";
import Direction from "./model/Direction.js";

const system = new ElevatorSystem(2); // nearest-car + LOOK

console.log("=== Requests coming in ===");
system.requestHallCall(5, Direction.UP);   // someone at floor 5 going up
system.requestHallCall(2, Direction.UP);   // someone at floor 2 going up
system.requestCarCall(1, 8);               // passenger in car 1 wants floor 8
system.requestHallCall(9, Direction.DOWN); // someone at floor 9 going down

console.log("\nInitial state:");
system.printState();

console.log("\n=== Simulation ===");
let tick = 0;
while (!system.allIdle() && tick < 30) {
    tick++;
    console.log(`\n[tick ${tick}]`);
    system.step();
    system.printState();
}

console.log(`\n=== All requests served in ${tick} ticks ===`);

Sequence Diagram — Hall Call

sequenceDiagram
    participant User
    participant Sys as ElevatorSystem
    participant D as DispatchStrategy
    participant Car as ElevatorCar
    participant M as MovementStrategy

    User->>Sys: requestHallCall(5, UP)
    Sys->>D: selectCar(cars, request)
    D-->>Sys: Car 1 (nearest)
    Sys->>Car: addStop(5)
    loop each tick
        Sys->>Car: step(movement)
        Car->>M: nextFloor(this)
        M-->>Car: 5
        Car->>Car: move one floor toward 5
    end
    Car-->>User: doors open at floor 5

How to Extend

Extension Implementation
Zone dispatch (low/high floors) New ZoneDispatch implements DispatchStrategy
SCAN instead of LOOK New ScanStrategy that runs to the building ends before reversing
Priority / VIP requests Wrap Request as a Command with priority; car serves a priority queue
Capacity limits Add load/maxLoad to the car; dispatcher skips full cars
Anti-starvation Age requests; boost a stop’s priority once it’s waited too long
Real-time (async) A scheduler thread calls step() on a fixed clock; guard car state with locks

What Interviewers Look For

  1. Two strategies, cleanly separated — dispatch (which car) vs movement (next floor)
  2. LOOK/SCAN scheduling — not “serve in arrival order”, which thrashes direction
  3. Sorted stop-setsTreeSet.ceiling()/floor() for O(log n) next-stop, not a linear scan
  4. Direction as state — a car serves the up-set ascending, down-set descending
  5. Facade — one clean entry point, wiring hidden
  6. No starvation story — you can articulate how a skipped request eventually gets served
  7. Runnable demo — a tick loop that visibly serves a mix of calls

Discussion

Newest first
You

Free system design + DSA prep. If it helped you crack an interview, consider supporting.