🧠 Elevator System Low-Level Design (LLD) – OOP, UML & Best Practices
An Elevator System is a fundamental Low-Level Design (LLD) problem frequently featured in technical interviews at top engineering organizations. The primary challenge involves designing a scalable, extensible, and modular system that manages multiple elevators across multiple floors while optimizing movement algorithms to minimize user wait times and avoid starvation.
1. Problem Statement & Core Requirements
An elevator system transports passengers between floors within a building. The system must process both external requests (made outside the elevator car) and internal requests (made inside the car) while managing elevator states safely and efficiently.
Primary Functional Requirements
- Multiple Elevators & Floors: The system manages a building containing floors and elevators (e.g., standard, express, service).
- External Floor Panel: On each floor, users can press external buttons (UP or DOWN) to call an elevator car.
- Internal Control Panel: Inside each elevator car, passengers select destination floors via a number pad.
- Real-time Indicators & Displays: Each elevator displays its current floor and movement direction to both internal passengers and external waiting halls.
- Optimal Scheduling Algorithm: The system dispatches and schedules elevator cars to minimize passenger wait and transit times while avoiding request starvation.
Non-Functional & Safety Requirements
- Extensibility: Simple addition of new dispatch strategies (e.g., energy-efficient, peak-hour) or elevator types (e.g., service elevators) via standard design patterns.
- State Management: Robust tracking of individual elevator states (
IDLE,MOVING,STOPPED,MAINTENANCE). - Safety Edge Cases: Accommodation of overload detection, emergency stops, and sensor inputs.
2. Scope Clarification & Mental Hooks
Real-Life Analogy: Traditional vs. Modern External Destination Keypads
- Traditional System (In-Scope): Outside the elevator, a user sees only two directional buttons (UP / DOWN). The destination floor is specified inside the car using the number panel.
- Destination Dispatch System (Out-of-Scope): Modern high-rise offices (e.g., Goldman Sachs) feature full numeric keypads outside the elevator, assigning a specific car before entry.
Note: In this lecture, we focus strictly on the Traditional System (directional panel outside, floor destination panel inside), which remains the standard for LLD interviews.
3. System Workflow & Conceptual Flow
When interacting with an elevator system, the execution flow operates across external requests, controller evaluation, movement scheduling, and internal selection:
4. Core Domain Entities & Key Enums
The domain model relies on well-defined states and key domain entities:
Key Domain Enums
public enum Direction {
UP,
DOWN,
IDLE
}
public enum ElevatorState {
IDLE,
MOVING,
STOPPED,
MAINTENANCE
}
5. Architectural Design Patterns
To make the codebase production-ready, modular, and extensible, we incorporate four primary software design patterns:
6. Class Diagram & Structural Relationships
The structural layout shows how the controller, entities, patterns, and strategies interlock:
7. Deep Dive into Elevator Scheduling Algorithms
The primary algorithm determines how elevators select their next stop given a mix of internal drop-off requests and external pick-up requests.
Benchmark Dry-Run Dataset
Suppose an elevator car is currently IDLE at Floor 5.
- Internal Requests pressed inside car at Floor 5 (in order):
[-1, 8, 2, 6, -5, 10] - External Requests triggered concurrently outside:
- Floor 3 requesting DOWN
- Floor 4 requesting UP
- Floor -8 requesting DOWN
Strategy 1: First Come, First Served (FCFS)
Mechanism
Executes requests strictly in the exact order they arrive in the request queue, without regard to current position or direction of travel.
Traversal Order for Benchmark Dataset
5 -> -1 -> 8 -> 2 -> 6 -> -5 -> 10 -> 3 -> 4 -> -8
Pros & Cons
- Pros: Simple to implement using a standard Queue ( enqueue/dequeue).
- Cons: Inefficient; causes wild vertical thrashing across floors, extreme power consumption, and long user wait times.
Strategy 2: SCAN (Elevator Algorithm)
Mechanism
The elevator travels completely to the furthest floor in one direction (sweeping all intermediate requests along the path) before reversing direction to service requests in the opposite direction.
Traversal Order (Assuming initial movement DOWN from Floor 5)
- DOWN Sweep: Services intermediate lower floors (
4*, 3*, 2, -1, -5, -8) until hitting the bottom-most floor request. - Direction Switch to UP: Sweeps upward (
6, 8, 10).
Note on
4*and3*: Depending on direction matching, SCAN sweeps all requests in the current path.
Pros & Cons
- Pros: Eliminates wild thrashing; easy to model with priority queues.
- Cons: Starvation Risk — If new requests continuously arrive in the current sweeping direction, requests in the opposite direction stall indefinitely. Furthermore, it forces the car to sweep to boundary limits unnecessarily.
Strategy 3: LOOK Strategy (Optimized Sweeping)
Mechanism
LOOK is an enhancement of SCAN. Instead of sweeping all the way to the building's physical limits or continuing indefinitely in one direction, LOOK checks if there are further requests ahead in the current direction. If no further requests exist in that direction, it immediately reverses.
Additionally, LOOK differentiates between internal drop-offs and external directional pick-ups:
- When traveling DOWN, it services internal drop-offs and DOWN-bound external requests.
- External UP-bound requests encountered while moving DOWN are bypassed until the car reverses direction (preventing unnecessary delays for current occupants).
Algorithm Comparison Table
8. Step-by-Step Code Implementation
1. Observer Pattern Implementation
public interface ElevatorObserver {
void onFloorChange(int elevatorId, int newFloor);
void onStateChange(int elevatorId, ElevatorState newState);
}
public class ElevatorDisplay implements ElevatorObserver {
private final int displayId;
public ElevatorDisplay(int displayId) {
this.displayId = displayId;
}
@Override
public void onFloorChange(int elevatorId, int newFloor) {
System.out.println("[Display #" + displayId + "] Elevator " + elevatorId + " reached Floor: " + newFloor);
}
@Override
public void onStateChange(int elevatorId, ElevatorState newState) {
System.out.println("[Display #" + displayId + "] Elevator " + elevatorId + " state updated to: " + newState);
}
}
2. Command Pattern Implementation
public class ElevatorRequest {
private final int targetFloor;
private final Direction requestedDirection; // Relevant for External Requests
private final boolean isInternal;
// Constructor for Internal Requests (Destination Floor only)
public ElevatorRequest(int targetFloor) {
this.targetFloor = targetFloor;
this.requestedDirection = Direction.IDLE;
this.isInternal = true;
}
// Constructor for External Requests (Pickup Floor + Direction)
public ElevatorRequest(int targetFloor, Direction requestedDirection) {
this.targetFloor = targetFloor;
this.requestedDirection = requestedDirection;
this.isInternal = false;
}
public int getTargetFloor() { return targetFloor; }
public Direction getRequestedDirection() { return requestedDirection; }
public boolean isInternal() { return isInternal; }
}
3. Strategy Pattern Implementation (LOOK Strategy)
import java.util.List;
public interface SchedulingStrategy {
Integer getNextStop(int currentFloor, Direction currentDirection, List<ElevatorRequest> requests);
}
public class LookSchedulingStrategy implements SchedulingStrategy {
@Override
public Integer getNextStop(int currentFloor, Direction currentDirection, List<ElevatorRequest> requests) {
if (requests.isEmpty()) {
return currentFloor;
}
ElevatorRequest primaryRequest = requests.get(0);
int primaryTarget = primaryRequest.getTargetFloor();
// Determine travel direction toward primary target
Direction travelDirection = (primaryTarget > currentFloor) ? Direction.UP : Direction.DOWN;
if (primaryTarget == currentFloor) {
return currentFloor;
}
Integer candidateFloor = null;
int minDistance = Integer.MAX_VALUE;
for (ElevatorRequest req : requests) {
int reqFloor = req.getTargetFloor();
if (travelDirection == Direction.UP) {
// Check intermediate floors above us up to primaryTarget
if (reqFloor >= currentFloor && reqFloor <= primaryTarget) {
if (req.isInternal() || req.getRequestedDirection() == Direction.UP) {
int dist = reqFloor - currentFloor;
if (dist < minDistance) {
minDistance = dist;
candidateFloor = reqFloor;
}
}
}
} else if (travelDirection == Direction.DOWN) {
// Check intermediate floors below us down to primaryTarget
if (reqFloor <= currentFloor && reqFloor >= primaryTarget) {
if (req.isInternal() || req.getRequestedDirection() == Direction.DOWN) {
int dist = currentFloor - reqFloor;
if (dist < minDistance) {
minDistance = dist;
candidateFloor = reqFloor;
}
}
}
}
}
return (candidateFloor != null) ? candidateFloor : primaryTarget;
}
}
4. Core Elevator Entity
import java.util.ArrayList;
import java.util.List;
public class Elevator {
private final int id;
private int currentFloor;
private Direction direction;
private ElevatorState state;
private final List<ElevatorRequest> requestQueue;
private final List<ElevatorObserver> observers;
public Elevator(int id, int initialFloor) {
this.id = id;
this.currentFloor = initialFloor;
this.direction = Direction.IDLE;
this.state = ElevatorState.IDLE;
this.requestQueue = new ArrayList<>();
this.observers = new ArrayList<>();
}
public void addObserver(ElevatorObserver observer) {
observers.add(observer);
}
public void addRequest(ElevatorRequest request) {
requestQueue.add(request);
if (this.state == ElevatorState.IDLE) {
this.state = ElevatorState.MOVING;
notifyStateChange();
}
}
public void moveToFloor(int newFloor) {
if (this.currentFloor != newFloor) {
this.direction = (newFloor > this.currentFloor) ? Direction.UP : Direction.DOWN;
this.currentFloor = newFloor;
notifyFloorChange();
}
}
public void completeArrival() {
this.state = ElevatorState.STOPPED;
notifyStateChange();
// Remove satisfied requests for current floor
requestQueue.removeIf(req -> req.getTargetFloor() == this.currentFloor);
if (requestQueue.isEmpty()) {
this.direction = Direction.IDLE;
this.state = ElevatorState.IDLE;
notifyStateChange();
} else {
this.state = ElevatorState.MOVING;
notifyStateChange();
}
}
private void notifyFloorChange() {
for (ElevatorObserver obs : observers) {
obs.onFloorChange(id, currentFloor);
}
}
private void notifyStateChange() {
for (ElevatorObserver obs : observers) {
obs.onStateChange(id, state);
}
}
// Getters
public int getId() { return id; }
public int getCurrentFloor() { return currentFloor; }
public Direction getDirection() { return direction; }
public ElevatorState getState() { return state; }
public List<ElevatorRequest> getRequestQueue() { return requestQueue; }
}
5. Elevator Controller & Building Orchestrator
import java.util.List;
public class ElevatorController {
private final List<Elevator> elevators;
private final SchedulingStrategy schedulingStrategy;
public ElevatorController(List<Elevator> elevators, SchedulingStrategy schedulingStrategy) {
this.elevators = elevators;
this.schedulingStrategy = schedulingStrategy;
}
public void handleExternalRequest(int pickupFloor, Direction direction, int elevatorId) {
Elevator elevator = getElevatorById(elevatorId);
if (elevator != null) {
elevator.addRequest(new ElevatorRequest(pickupFloor, direction));
}
}
public void handleInternalRequest(int destinationFloor, int elevatorId) {
Elevator elevator = getElevatorById(elevatorId);
if (elevator != null) {
elevator.addRequest(new ElevatorRequest(destinationFloor));
}
}
public void stepSimulation() {
for (Elevator elevator : elevators) {
if (!elevator.getRequestQueue().isEmpty()) {
int nextStop = schedulingStrategy.getNextStop(
elevator.getCurrentFloor(),
elevator.getDirection(),
elevator.getRequestQueue()
);
if (nextStop != elevator.getCurrentFloor()) {
elevator.moveToFloor(nextStop);
} else {
elevator.completeArrival();
}
}
}
}
private Elevator getElevatorById(int id) {
return elevators.stream().filter(e -> e.getId() == id).findFirst().orElse(null);
}
}
9. Simulation & Execution Flow
Below is a complete executable driver program that demonstrates the interactive workflow using the LOOK strategy:
import java.util.Collections;
public class ElevatorSystemMain {
public static void main(String[] args) {
// Setup System Entities
Elevator elevator1 = new Elevator(1, 5); // Elevator 1 starts at Floor 5
ElevatorDisplay display = new ElevatorDisplay(101);
elevator1.addObserver(display);
SchedulingStrategy lookStrategy = new LookSchedulingStrategy();
ElevatorController controller = new ElevatorController(
Collections.singletonList(elevator1),
lookStrategy
);
System.out.println("--- Submitting Internal & External Requests ---");
// Internal requests pressed inside car at Floor 5
controller.handleInternalRequest(-1, 1);
controller.handleInternalRequest(8, 1);
controller.handleInternalRequest(2, 1);
// External requests triggered from hall panels
controller.handleExternalRequest(3, Direction.DOWN, 1);
controller.handleExternalRequest(4, Direction.UP, 1);
System.out.println("\n--- Executing Step-by-Step Simulation ---");
for (int step = 1; step <= 10; step++) {
System.out.println("\n[Time Step " + step + "]");
controller.stepSimulation();
}
}
}
10. System Extensibility & Advanced Patterns
Adding New Elevator Types via Factory Pattern
When extending the system to support specialized elevator types (e.g., Express Elevators that bypass lower floors, or heavy-duty Service Elevators), the Factory Pattern decouples object instantiation from the core controller.
public abstract class BaseElevator extends Elevator {
public BaseElevator(int id, int initialFloor) {
super(id, initialFloor);
}
public abstract int getMaxWeightCapacity();
}
public class ExpressElevator extends BaseElevator {
public ExpressElevator(int id, int initialFloor) {
super(id, initialFloor);
}
@Override
public int getMaxWeightCapacity() { return 1000; } // Higher capacity, higher speed
}
public class ElevatorFactory {
public static Elevator createElevator(String type, int id, int initialFloor) {
switch (type.toUpperCase()) {
case "EXPRESS":
return new ExpressElevator(id, initialFloor);
case "STANDARD":
default:
return new Elevator(id, initialFloor);
}
}
}
11. Real-World Edge Cases & Production Reality
Summary and Key Takeaways
- Core Objective: The Low-Level Design of an elevator system balances clear Object-Oriented design with robust movement scheduling algorithms.
- Interface Decoupling: The Observer Pattern decouples elevator state notifications from physical display panels or control room monitors.
- Command Pattern Usage: Encapsulating internal and external requests via
ElevatorRequestunifies request handling and queue management. - Strategy Pattern Flexibility: Plugging in
SchedulingStrategyimplementations (FCFS, SCAN, LOOK) allows runtime swapping of scheduling behaviors. - LOOK Strategy Advantage: Unlike naive SCAN (which sweeps to physical boundaries), LOOK continuously inspects pending requests ahead in the current travel direction, switching directions as soon as further requests in that direction are exhausted.
- Production-Grade Extensibility: Incorporating an
ElevatorFactoryensures simple creation of specialized elevator types (Express, Service) without disrupting existing dispatch controllers.