The elevator system is a favorite for senior-level LLD interviews. It’s a real-world system that naturally involves the State pattern, Strategy pattern, and concurrency thinking. We’re designing a building with multiple elevators that handle floor requests efficiently.
What makes this question tricky is the scheduling — when someone presses “up” on floor 5, which elevator should respond? That decision-making logic is where the real design lives.
Requirements
Functional:
- Building has N floors and M elevators
- External requests: person on a floor presses UP or DOWN button
- Internal requests: person inside elevator presses a floor button
- Elevator moves up/down, opens/closes doors, picks up passengers
- Select the optimal elevator for each external request
Assumptions:
- Elevators use the LOOK algorithm (serve requests in current direction, then reverse)
- No weight limit for now (common extension)
- Doors open automatically when elevator reaches a requested floor
Key Classes & Relationships
- scheduler: SchedulingStrategy
+ handle_request(floor, direction)
- state: ElevatorState
- requests: SortedSet
Core Implementation
from abc import ABC, abstractmethod
from enum import Enum
# ---- Enums ----
class Direction(Enum):
UP = "UP"
DOWN = "DOWN"
IDLE = "IDLE"
class DoorState(Enum):
OPEN = "OPEN"
CLOSED = "CLOSED"
# ---- Door & Display ----
class Door:
def __init__(self):
self.state = DoorState.CLOSED
def open(self):
self.state = DoorState.OPEN
print(" Door opened")
def close(self):
self.state = DoorState.CLOSED
print(" Door closed")
class Display:
def __init__(self):
self.floor = 0
self.direction = Direction.IDLE
def update(self, floor: int, direction: Direction):
self.floor = floor
self.direction = direction
# ---- Elevator ----
class Elevator:
def __init__(self, elevator_id: int):
self.id = elevator_id
self.current_floor = 0
self.direction = Direction.IDLE
self.door = Door()
self.display = Display()
self.up_requests = set() # floors to visit going up
self.down_requests = set() # floors to visit going down
def add_request(self, floor: int, direction: Direction = None):
"""Add a floor request. Direction helps decide which set to add to."""
if floor > self.current_floor:
self.up_requests.add(floor)
elif floor < self.current_floor:
self.down_requests.add(floor)
else:
# Already on this floor -- just open doors
self._stop_at_floor()
def _stop_at_floor(self):
self.door.open()
self.display.update(self.current_floor, self.direction)
print(f" Elevator {self.id} stopped at floor {self.current_floor}")
self.door.close()
def move(self):
"""Process one step of movement using LOOK algorithm."""
if not self.up_requests and not self.down_requests:
self.direction = Direction.IDLE
return
if self.direction == Direction.IDLE:
# Pick whichever direction has requests
self.direction = Direction.UP if self.up_requests else Direction.DOWN
if self.direction == Direction.UP:
if self.up_requests:
next_floor = min(self.up_requests)
self._move_to(next_floor)
self.up_requests.discard(next_floor)
else:
# No more up requests, reverse
self.direction = Direction.DOWN
self.move()
elif self.direction == Direction.DOWN:
if self.down_requests:
next_floor = max(self.down_requests)
self._move_to(next_floor)
self.down_requests.discard(next_floor)
else:
self.direction = Direction.UP
self.move()
def _move_to(self, floor: int):
print(f" Elevator {self.id}: floor {self.current_floor} -> {floor}")
self.current_floor = floor
self._stop_at_floor()
def pending_count(self) -> int:
return len(self.up_requests) + len(self.down_requests)
# ---- Scheduling Strategy ----
class SchedulingStrategy(ABC):
@abstractmethod
def select_elevator(self, elevators: list, floor: int, direction: Direction) -> "Elevator":
pass
class NearestElevatorStrategy(SchedulingStrategy):
"""Pick the closest idle or same-direction elevator."""
def select_elevator(self, elevators, floor, direction):
best = None
best_score = float('inf')
for elevator in elevators:
# Prefer idle elevators or ones going the same direction
if elevator.direction == Direction.IDLE:
score = abs(elevator.current_floor - floor)
elif elevator.direction == direction:
diff = floor - elevator.current_floor
# Only good if the elevator hasn't passed us yet
if direction == Direction.UP and diff >= 0:
score = diff
elif direction == Direction.DOWN and diff <= 0:
score = abs(diff)
else:
score = float('inf') # It'll have to come back
else:
score = float('inf') # Going the wrong way
if score < best_score:
best_score = score
best = elevator
# If no good option, pick least busy
if best_score == float('inf'):
best = min(elevators, key=lambda e: e.pending_count())
return best
# ---- Elevator System ----
class ElevatorSystem:
def __init__(self, num_elevators: int, num_floors: int,
strategy: SchedulingStrategy = None):
self.elevators = [Elevator(i) for i in range(num_elevators)]
self.num_floors = num_floors
self.strategy = strategy or NearestElevatorStrategy()
def handle_external_request(self, floor: int, direction: Direction):
"""Someone pressed UP/DOWN on a floor."""
elevator = self.strategy.select_elevator(self.elevators, floor, direction)
print(f"Request: floor {floor} {direction.value} -> assigned to Elevator {elevator.id}")
elevator.add_request(floor, direction)
def handle_internal_request(self, elevator_id: int, floor: int):
"""Someone inside an elevator pressed a floor button."""
self.elevators[elevator_id].add_request(floor)
def step(self):
"""Move all elevators one step. Call this in a loop or on a timer."""
for elevator in self.elevators:
elevator.move()
# ---- Usage ----
system = ElevatorSystem(num_elevators=3, num_floors=10)
system.handle_external_request(5, Direction.UP) # Someone on floor 5 going up
system.handle_internal_request(0, 8) # Inside elevator 0, press 8
system.handle_external_request(2, Direction.DOWN) # Someone on floor 2 going down
system.step() # All elevators process one movement
system.step() # Continue processing
// ---- Enums ----
const Direction = Object.freeze({ UP: "UP", DOWN: "DOWN", IDLE: "IDLE" });
const DoorState = Object.freeze({ OPEN: "OPEN", CLOSED: "CLOSED" });
// ---- Door & Display ----
class Door {
constructor() { this.state = DoorState.CLOSED; }
open() { this.state = DoorState.OPEN; console.log(" Door opened"); }
close() { this.state = DoorState.CLOSED; console.log(" Door closed"); }
}
class Display {
constructor() { this.floor = 0; this.direction = Direction.IDLE; }
update(floor, direction) { this.floor = floor; this.direction = direction; }
}
// ---- Elevator ----
class Elevator {
constructor(id) {
this.id = id;
this.currentFloor = 0;
this.direction = Direction.IDLE;
this.door = new Door();
this.display = new Display();
this.upRequests = new Set();
this.downRequests = new Set();
}
addRequest(floor) {
if (floor > this.currentFloor) this.upRequests.add(floor);
else if (floor < this.currentFloor) this.downRequests.add(floor);
else this.#stopAtFloor();
}
#stopAtFloor() {
this.door.open();
this.display.update(this.currentFloor, this.direction);
console.log(` Elevator ${this.id} stopped at floor ${this.currentFloor}`);
this.door.close();
}
move() {
if (!this.upRequests.size && !this.downRequests.size) {
this.direction = Direction.IDLE;
return;
}
if (this.direction === Direction.IDLE) {
this.direction = this.upRequests.size ? Direction.UP : Direction.DOWN;
}
if (this.direction === Direction.UP) {
if (this.upRequests.size) {
const next = Math.min(...this.upRequests);
this.#moveTo(next);
this.upRequests.delete(next);
} else {
this.direction = Direction.DOWN;
this.move();
}
} else if (this.direction === Direction.DOWN) {
if (this.downRequests.size) {
const next = Math.max(...this.downRequests);
this.#moveTo(next);
this.downRequests.delete(next);
} else {
this.direction = Direction.UP;
this.move();
}
}
}
#moveTo(floor) {
console.log(` Elevator ${this.id}: floor ${this.currentFloor} -> ${floor}`);
this.currentFloor = floor;
this.#stopAtFloor();
}
get pendingCount() { return this.upRequests.size + this.downRequests.size; }
}
// ---- Scheduling Strategy ----
class NearestElevatorStrategy {
selectElevator(elevators, floor, direction) {
let best = null, bestScore = Infinity;
for (const elev of elevators) {
let score;
if (elev.direction === Direction.IDLE) {
score = Math.abs(elev.currentFloor - floor);
} else if (elev.direction === direction) {
const diff = floor - elev.currentFloor;
if (direction === Direction.UP && diff >= 0) score = diff;
else if (direction === Direction.DOWN && diff <= 0) score = Math.abs(diff);
else score = Infinity;
} else {
score = Infinity;
}
if (score < bestScore) { bestScore = score; best = elev; }
}
if (bestScore === Infinity) {
best = elevators.reduce((a, b) => a.pendingCount <= b.pendingCount ? a : b);
}
return best;
}
}
// ---- Elevator System ----
class ElevatorSystem {
constructor(numElevators, numFloors, strategy = new NearestElevatorStrategy()) {
this.elevators = Array.from({ length: numElevators }, (_, i) => new Elevator(i));
this.numFloors = numFloors;
this.strategy = strategy;
}
handleExternalRequest(floor, direction) {
const elev = this.strategy.selectElevator(this.elevators, floor, direction);
console.log(`Request: floor ${floor} ${direction} -> Elevator ${elev.id}`);
elev.addRequest(floor);
}
handleInternalRequest(elevatorId, floor) {
this.elevators[elevatorId].addRequest(floor);
}
step() { this.elevators.forEach(e => e.move()); }
}
// Usage
const system = new ElevatorSystem(3, 10);
system.handleExternalRequest(5, Direction.UP);
system.handleInternalRequest(0, 8);
system.step();
import java.util.*;
enum Direction { UP, DOWN, IDLE }
enum DoorState { OPEN, CLOSED }
class Door {
DoorState state = DoorState.CLOSED;
void open() { state = DoorState.OPEN; System.out.println(" Door opened"); }
void close() { state = DoorState.CLOSED; System.out.println(" Door closed"); }
}
class Display {
int floor = 0;
Direction direction = Direction.IDLE;
void update(int f, Direction d) { floor = f; direction = d; }
}
class Elevator {
int id, currentFloor = 0;
Direction direction = Direction.IDLE;
Door door = new Door();
Display display = new Display();
TreeSet<Integer> upRequests = new TreeSet<>();
TreeSet<Integer> downRequests = new TreeSet<>();
Elevator(int id) { this.id = id; }
void addRequest(int floor) {
if (floor > currentFloor) upRequests.add(floor);
else if (floor < currentFloor) downRequests.add(floor);
else stopAtFloor();
}
private void stopAtFloor() {
door.open();
display.update(currentFloor, direction);
System.out.printf(" Elevator %d stopped at floor %d%n", id, currentFloor);
door.close();
}
void move() {
if (upRequests.isEmpty() && downRequests.isEmpty()) {
direction = Direction.IDLE;
return;
}
if (direction == Direction.IDLE)
direction = !upRequests.isEmpty() ? Direction.UP : Direction.DOWN;
if (direction == Direction.UP) {
if (!upRequests.isEmpty()) { moveTo(upRequests.pollFirst()); }
else { direction = Direction.DOWN; move(); }
} else {
if (!downRequests.isEmpty()) { moveTo(downRequests.pollLast()); }
else { direction = Direction.UP; move(); }
}
}
private void moveTo(int floor) {
System.out.printf(" Elevator %d: floor %d -> %d%n", id, currentFloor, floor);
currentFloor = floor;
stopAtFloor();
}
int pendingCount() { return upRequests.size() + downRequests.size(); }
}
// ---- Scheduling Strategy ----
interface SchedulingStrategy {
Elevator selectElevator(List<Elevator> elevators, int floor, Direction dir);
}
class NearestElevatorStrategy implements SchedulingStrategy {
public Elevator selectElevator(List<Elevator> elevators, int floor, Direction dir) {
Elevator best = null;
int bestScore = Integer.MAX_VALUE;
for (Elevator e : elevators) {
int score;
if (e.direction == Direction.IDLE) {
score = Math.abs(e.currentFloor - floor);
} else if (e.direction == dir) {
int diff = floor - e.currentFloor;
if (dir == Direction.UP && diff >= 0) score = diff;
else if (dir == Direction.DOWN && diff <= 0) score = Math.abs(diff);
else score = Integer.MAX_VALUE;
} else {
score = Integer.MAX_VALUE;
}
if (score < bestScore) { bestScore = score; best = e; }
}
if (bestScore == Integer.MAX_VALUE) {
best = elevators.stream()
.min(Comparator.comparingInt(Elevator::pendingCount))
.orElse(elevators.get(0));
}
return best;
}
}
class ElevatorSystem {
List<Elevator> elevators;
SchedulingStrategy strategy;
ElevatorSystem(int numElevators, SchedulingStrategy strategy) {
this.elevators = new ArrayList<>();
for (int i = 0; i < numElevators; i++) elevators.add(new Elevator(i));
this.strategy = strategy;
}
void handleExternalRequest(int floor, Direction dir) {
Elevator e = strategy.selectElevator(elevators, floor, dir);
System.out.printf("Request: floor %d %s -> Elevator %d%n", floor, dir, e.id);
e.addRequest(floor);
}
void handleInternalRequest(int elevatorId, int floor) {
elevators.get(elevatorId).addRequest(floor);
}
void step() { elevators.forEach(Elevator::move); }
}
The LOOK Algorithm (Elevator Algorithm)
This is the core scheduling logic that interviewers want to see. Here’s how it works:
- The elevator moves in one direction (say UP)
- It serves all requests in that direction in order
- When no more requests in that direction, it reverses
- Repeat
It’s called LOOK because the elevator “looks ahead” — if there are no more requests above, it doesn’t go all the way to the top floor. It just reverses. This is how real elevators work.
We split requests into up_requests and down_requests sets. Going up? Serve from up_requests (lowest first). Going down? Serve from down_requests (highest first). Simple.
Design Patterns Used
Strategy — SchedulingStrategy lets us swap the elevator selection algorithm. NearestElevator is one approach. We could add RoundRobin, LeastBusy, or ZoneBasedScheduler without touching the ElevatorSystem class.
State — The elevator’s behavior changes based on direction (UP/DOWN/IDLE). In a more detailed implementation, we’d use the full State pattern with MovingUpState, MovingDownState, IdleState classes.
Observer (optional extension) — Floors could observe elevator positions to update their displays. We didn’t add it here to keep things focused.
Extensions
- Priority floors — Lobby and parking levels get priority. Add a weight to certain floors in the scheduling algorithm.
- Emergency mode — All elevators go to ground floor, doors open. Add an
EmergencyStatethat overrides normal behavior. - Weight limit — Elevator tracks current weight. If above threshold, skip pickup requests and only serve internal drop-off requests.
- Zone-based scheduling — Elevator 1 serves floors 1-10, Elevator 2 serves 11-20. Add a
ZoneSchedulerstrategy. - VIP elevator — Dedicated elevator for certain floors. Filter it out of the general pool in the scheduler.
In simple language, the elevator system is about two things: which elevator do we pick for a request, and how does each elevator decide where to go next. Strategy pattern handles the first question. The LOOK algorithm handles the second. Nail these two and the rest is just wiring classes together.