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
- A building has N floors and M elevator cars.
- 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.
- A dispatcher assigns each external request to the most suitable car.
- Each car serves its committed stops in an efficient order (SCAN / LOOK — keep going one way, then reverse).
- A car tracks its direction: UP, DOWN, or IDLE.
- Doors open on arrival at a committed floor.
Non-Functional Requirements
- No starvation — a request is eventually served, not indefinitely skipped.
- Extensibility — new dispatch policy or movement strategy = one new class.
- 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:
- Which car? →
DispatchStrategy. Nearest-car is the classic; you could do least-busy, or zone-based. - Next floor for a committed car? →
MovementStrategy. LOOK (a smarter SCAN) keeps moving in the current direction until no more stops lie ahead, then reverses — minimising direction changes.
💡 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
}
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 : "") + "]";
}
}
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;
}
}
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);
}
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;
}
}
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);
}
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;
}
}
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));
}
}
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 ===");
}
}
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
- ✅ Two strategies, cleanly separated — dispatch (which car) vs movement (next floor)
- ✅ LOOK/SCAN scheduling — not “serve in arrival order”, which thrashes direction
- ✅ Sorted stop-sets —
TreeSet.ceiling()/floor()for O(log n) next-stop, not a linear scan - ✅ Direction as state — a car serves the up-set ascending, down-set descending
- ✅ Facade — one clean entry point, wiring hidden
- ✅ No starvation story — you can articulate how a skipped request eventually gets served
- ✅ Runnable demo — a tick loop that visibly serves a mix of calls
Related Designs
- Vending Machine — State pattern for action handling
- Parking Lot — Strategy pattern for swappable pricing
- Snake & Ladder — turn-based simulation loop
Discussion
Newest first