Design Elevator System
Multi-elevator dispatch, request queues, and direction-aware scheduling — a classic at Microsoft, Amazon, Goldman, and many OOP-focused LLD rounds.
Model Request (floor + direction), Elevator state (floor, direction, stops), ElevatorController facade, and DispatchStrategy to pick the best car. Interviewers want SCAN/LOOK-style scheduling explained, then clean Java.
The interview question
“Design an elevator control system for a building.” Users press up/down at a floor or select a destination inside the car; the system assigns requests and moves elevators efficiently.
Focus on request model, elevator state, dispatch algorithm, and thread-safe step simulation — not building physics engines.
Clarifying questions (ask before coding)
Request with direction; car → destination-only stop.step() tick or processNextStop() — clarify interface.Requirements & assumptions (state aloud)
- Functional: submit hall/car requests; controller assigns to an elevator; elevator moves floor-by-floor serving stops in current direction.
- Elevator state: current floor,
Direction(UP/DOWN/IDLE), pending stop set. - Dispatch: Strategy picks elevator for a new external request.
- v1: single building, in-memory; synchronous
step()loop for demo. - Out of scope: weight limits, maintenance mode, fire service — extensions.
High-level design & class relationships
ElevatorController is the facade — requestElevator(floor, direction) and selectFloor(elevatorId, floor).
Each Elevator maintains a TreeSet (or sorted set) of stops and reverses direction when no more stops ahead.
- Enums:
Direction - Domain:
Request,Elevator - Services:
ElevatorController,DispatchStrategy
↑ UP ↓ DOWN — IDLE G ground lobby
UML class diagram (optional)
Sketch 5–6 classes if asked — controller, elevator, request, strategy.
Relationships to label aloud
- Composition:
ElevatorController→List<Elevator>. - Dependency:
ElevatorController→DispatchStrategy. - Value object:
Request(floor + direction + source).
Design patterns used (and why)
| Pattern | Where | Why (interview soundbite) |
|---|---|---|
| 🪞Facade | ElevatorController | Single entry for hall and car requests. |
| 🎯Strategy | DispatchStrategy | Swap nearest-car vs load-balanced dispatch. |
| 🎬State | Elevator direction + stops | Movement rules depend on UP/DOWN/IDLE. |
| 👁️Observer (ext) | floor display panels | Notify UI when elevator arrives at floor. |
Full solution — Request & Elevator
public enum Direction { UP, DOWN, IDLE }
public class Request {
public enum Source { HALL, CAR }
private final int floor;
private final Direction direction;
private final Source source;
private final Instant createdAt = Instant.now();
// Hall call: user pressed UP or DOWN at a floor
public static Request hallCall(int floor, Direction direction) {
return new Request(floor, direction, Source.HALL);
}
// Car call: passenger selected destination inside elevator
public static Request carCall(int destinationFloor) {
return new Request(destinationFloor, Direction.IDLE, Source.CAR);
}
// Private ctor — use factory methods for hall vs car semantics
private Request(int floor, Direction direction, Source source) {
this.floor = floor;
this.direction = direction;
this.source = source;
}
// Returns requested floor (hall) or destination (car)
public int getFloor() { return floor; }
// Returns UP/DOWN for hall calls; IDLE for car destination-only
public Direction getDirection() { return direction; }
// Distinguishes external hall button from in-car panel
public Source getSource() { return source; }
// Timestamp for FIFO tie-breaking or metrics
public Instant getCreatedAt() { return createdAt; }
}public class Elevator {
private final int id;
private final int minFloor;
private final int maxFloor;
private int currentFloor;
private Direction direction = Direction.IDLE;
private final TreeSet<Integer> upStops = new TreeSet<>();
private final TreeSet<Integer> downStops = new TreeSet<>(Comparator.reverseOrder());
// Creates elevator with id and servicable floor range
public Elevator(int id, int minFloor, int maxFloor, int startFloor) {
this.id = id;
this.minFloor = minFloor;
this.maxFloor = maxFloor;
this.currentFloor = startFloor;
}
// Adds a stop from hall or car request; sets direction if idle
public synchronized void addRequest(Request request) {
int floor = request.getFloor();
validateFloor(floor);
if (request.getSource() == Request.Source.CAR) {
addStop(floor);
return;
}
if (request.getDirection() == Direction.UP) upStops.add(floor);
else if (request.getDirection() == Direction.DOWN) downStops.add(floor);
if (direction == Direction.IDLE) {
direction = floor >= currentFloor ? Direction.UP : Direction.DOWN;
}
}
// Routes stop to up or down queue based on current direction context
private void addStop(int floor) {
if (floor > currentFloor || (floor == currentFloor && direction == Direction.UP))
upStops.add(floor);
else
downStops.add(floor);
if (direction == Direction.IDLE)
direction = floor >= currentFloor ? Direction.UP : Direction.DOWN;
}
// Moves one floor toward next stop; opens door if at stop; returns true if moved or stopped
public synchronized boolean step() {
if (shouldStopHere()) {
openDoors();
return true;
}
if (direction == Direction.UP && !upStops.isEmpty()) {
currentFloor++;
return true;
}
if (direction == Direction.DOWN && !downStops.isEmpty()) {
currentFloor--;
return true;
}
switchDirectionIfNeeded();
return direction != Direction.IDLE;
}
// True when current floor is in the active direction's stop set
private boolean shouldStopHere() {
return (direction == Direction.UP && upStops.contains(currentFloor))
|| (direction == Direction.DOWN && downStops.contains(currentFloor));
}
// Removes current floor from queues and goes IDLE if no more stops
private void openDoors() {
upStops.remove(currentFloor);
downStops.remove(currentFloor);
switchDirectionIfNeeded();
}
// Flips UP↔DOWN or IDLE when current direction queue is empty
private void switchDirectionIfNeeded() {
if (direction == Direction.UP && upStops.isEmpty()) {
direction = downStops.isEmpty() ? Direction.IDLE : Direction.DOWN;
} else if (direction == Direction.DOWN && downStops.isEmpty()) {
direction = upStops.isEmpty() ? Direction.IDLE : Direction.UP;
} else if (direction == Direction.IDLE) {
if (!upStops.isEmpty()) direction = Direction.UP;
else if (!downStops.isEmpty()) direction = Direction.DOWN;
}
}
// Throws if floor outside building range
private void validateFloor(int floor) {
if (floor < minFloor || floor > maxFloor)
throw new IllegalArgumentException("Invalid floor: " + floor);
}
// Estimated cost to serve a hall request — used by dispatch strategies
public synchronized int estimatedCost(Request request) {
int target = request.getFloor();
int distance = Math.abs(currentFloor - target);
if (direction == Direction.IDLE) return distance;
if (direction == Direction.UP && target >= currentFloor) return distance;
if (direction == Direction.DOWN && target <= currentFloor) return distance;
return distance + 10; // penalty if elevator must reverse first
}
// True when no pending stops and direction is IDLE
public synchronized boolean isIdle() {
return direction == Direction.IDLE && upStops.isEmpty() && downStops.isEmpty();
}
// Returns elevator id for controller routing
public int getId() { return id; }
// Returns current floor for display and dispatch
public int getCurrentFloor() { return currentFloor; }
// Returns movement direction for scheduling logic
public Direction getDirection() { return direction; }
}Full solution — Controller & dispatch
public interface DispatchStrategy {
// Returns best elevator to handle this hall request
Elevator selectElevator(Request request, List<Elevator> elevators);
}
public class NearestElevatorStrategy implements DispatchStrategy {
// Picks elevator with lowest estimatedCost; prefers idle cars on ties
@Override
public Elevator selectElevator(Request request, List<Elevator> elevators) {
return elevators.stream()
.min(Comparator
.comparingInt((Elevator e) -> e.estimatedCost(request))
.thenComparing(e -> e.isIdle() ? 0 : 1))
.orElseThrow();
}
}
public class LookScanStrategy implements DispatchStrategy {
// Same cost function but strongly prefers elevators already moving toward request
@Override
public Elevator selectElevator(Request request, List<Elevator> elevators) {
int floor = request.getFloor();
Direction dir = request.getDirection();
Optional<Elevator> movingMatch = elevators.stream()
.filter(e -> e.getDirection() == dir)
.filter(e -> dir == Direction.UP ? e.getCurrentFloor() <= floor : e.getCurrentFloor() >= floor)
.min(Comparator.comparingInt(e -> Math.abs(e.getCurrentFloor() - floor)));
return movingMatch.orElseGet(() -> new NearestElevatorStrategy().selectElevator(request, elevators));
}
}public class ElevatorController {
private final List<Elevator> elevators;
private final DispatchStrategy dispatchStrategy;
private final int minFloor;
private final int maxFloor;
// Builds controller with floor bounds, elevator fleet, and dispatch policy
public ElevatorController(int minFloor, int maxFloor, List<Elevator> elevators, DispatchStrategy dispatchStrategy) {
this.minFloor = minFloor;
this.maxFloor = maxFloor;
this.elevators = List.copyOf(elevators);
this.dispatchStrategy = dispatchStrategy;
}
// Hall button pressed — validates floor/direction and routes to best elevator
public void requestElevator(int floor, Direction direction) {
validateFloor(floor);
if (direction == Direction.IDLE)
throw new IllegalArgumentException("Hall call needs UP or DOWN");
Request req = Request.hallCall(floor, direction);
Elevator chosen = dispatchStrategy.selectElevator(req, elevators);
chosen.addRequest(req);
}
// In-car destination button — adds stop to specific elevator
public void selectFloor(int elevatorId, int destinationFloor) {
validateFloor(destinationFloor);
Elevator e = findElevator(elevatorId);
e.addRequest(Request.carCall(destinationFloor));
}
// Advances all elevators one simulation tick; returns true if any car moved
public boolean stepAll() {
boolean any = false;
for (Elevator e : elevators) {
if (e.step()) any = true;
}
return any;
}
// Returns read-only fleet for monitoring dashboards
public List<Elevator> getElevators() {
return Collections.unmodifiableList(elevators);
}
// Finds elevator by id or throws if unknown
private Elevator findElevator(int id) {
return elevators.stream()
.filter(e -> e.getId() == id)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unknown elevator: " + id));
}
// Ensures floor is within building bounds
private void validateFloor(int floor) {
if (floor < minFloor || floor > maxFloor)
throw new IllegalArgumentException("Invalid floor: " + floor);
}
}
public class ElevatorDemo {
// Simulates hall call at floor 3 UP and runs until all cars idle
public static void main(String[] args) {
List<Elevator> cars = List.of(
new Elevator(1, 0, 10, 0),
new Elevator(2, 0, 10, 5)
);
ElevatorController controller = new ElevatorController(
0, 10, cars, new NearestElevatorStrategy());
controller.requestElevator(3, Direction.UP);
controller.selectFloor(1, 7);
for (int i = 0; i < 20 && controller.stepAll(); i++) {
// simulation tick
}
controller.getElevators().forEach(e ->
System.out.println("Elevator " + e.getId() + " at F" + e.getCurrentFloor()));
}
}While you code — questions interviewers ask
Extensions & follow-ups
- Priority: fire service / VIP override queue.
- Weight sensor: skip floor when car full.
- Multiple banks: separate controllers per shaft group.
- Predictive dispatch: ML on traffic patterns — mention only if senior loop.
Common mistakes (fail signals)
- No direction reversal — elevator stuck after one trip.
- Dispatch always to elevator 0 — ignores cost/wait time.
- Mixing hall and car requests without
Request.Source. - Using unsorted list for stops — O(n) scan every floor.
- Forgetting bounds check on floor numbers.