Design an Elevator System
OOP design for a multi-elevator system covering scheduling algorithms (SCAN, LOOK, nearest-car), request queuing, direction management, door state machines, and capacity handling.
The Problem
Your company manages a 40-story office tower with six elevators. Tenants complain every morning: all six cars cluster near the lobby, upper-floor workers wait four minutes for a ride, and during fire drills nobody knows which elevators to lock down. The building manager says the old controller "just sends the nearest car" but that strategy starves anyone above floor 20 when the lobby is busy.
An elevator system is deceptively complex. Each car has a physical state (moving, stopped, doors open), a direction it is committed to, a queue of pending requests, weight constraints, and a door that cycles through its own state machine independently. Then multiply that by six cars and add a dispatcher that must decide which car handles which request. The scheduling algorithm is the heart of the problem, and the wrong choice means everyone waits.
Design the core classes for a multi-elevator system that handles external requests (floor button with direction), internal requests (destination floor inside the car), pluggable dispatching strategies, elevator state management, door state machines, direction-aware request queuing, and capacity enforcement.
Requirements
Clarifying Questions
Before jumping into class design, ask questions to turn the vague prompt into a concrete specification. Cover four areas: core actions, error handling, boundaries, and future extensions.
You: "How many elevators and floors are we designing for? Is this configurable?"
Interviewer: "Multiple elevators, configurable count. The building has N floors, also configurable. Think 6 elevators and 40 floors as the default scenario."
Good. Both are constructor parameters, not hardcoded. That means the system must work for any combination.
You: "What types of requests does the system handle? Just 'go to floor 5'?"
Interviewer: "Two types. External requests come from floor buttons: a person on floor 12 presses UP. Internal requests come from inside the car: a passenger presses the button for floor 30. External requests have a floor and direction. Internal requests have just a destination floor."
Two distinct request types. External requests carry direction intent, internal requests are just a target floor. The dispatcher uses external requests to pick a car; internal requests go straight to the assigned car's queue.
You: "How should the system decide which elevator handles an external request?"
Interviewer: "Support multiple strategies. At minimum: nearest idle car, zone-based assignment, and a SCAN-based approach where a car already moving in the right direction picks it up."
Multiple dispatch strategies with runtime selection. Strategy pattern for the dispatcher. The SCAN approach is the most interesting because it mirrors the classic elevator algorithm from operating systems.
You: "When an elevator is moving up, should it serve all upward requests before reversing?"
Interviewer: "Yes, that is the expected behavior. The car serves all requests in its current direction, then reverses. This is the SCAN or elevator algorithm."
Direction commitment is key. An upward-bound car on floor 5 with requests for floors 8, 15, and 3 serves 8 and 15 first, then reverses to serve 3. This prevents starvation of higher floors.
You: "What about the car doors? Do we need to model the full open/close cycle?"
Interviewer: "Yes. Doors transition through CLOSED, OPENING, OPEN, and CLOSING states. The elevator cannot move while doors are open. If someone requests the door to open while closing, it should reverse to OPENING."
A four-state door state machine. The constraint that the car cannot move while doors are not fully closed is a critical safety invariant. Door reversal on obstruction is a real-world requirement.
You: "Is there a weight or passenger capacity limit?"
Interviewer: "Yes. Each elevator has a maximum weight capacity. If adding a passenger would exceed it, the car should signal an overweight alarm and refuse to move until weight drops below the limit."
Capacity enforcement. The car tracks current weight, and the system blocks movement when overloaded. This affects the dispatching logic too: do not assign more passengers to an already-full car.
You: "Should we handle emergency mode, VIP elevators, or freight elevators?"
Interviewer: "Out of scope for the core design, but the architecture should make those easy to add later."
Perfect. You have now clarified scope and ruled out unnecessary complexity.
Final Requirements
Functional Requirements:
- External requests: a person on floor F presses UP or DOWN; the dispatcher assigns the best elevator
- Internal requests: a passenger inside elevator E presses floor F; the request is added to that car's queue
- Elevators serve all requests in the current direction before reversing (SCAN algorithm)
- Doors cycle through CLOSED, OPENING, OPEN, CLOSING states; the car cannot move unless doors are CLOSED
- Door reversal: if a door-open request arrives while CLOSING, transition back to OPENING
- Weight capacity: each elevator tracks current load and refuses to move when overloaded
- The dispatcher is pluggable: support nearest-idle, zone-based, and SCAN-aware strategies
Non-Functional Requirements:
- Thread safety: multiple floor buttons can be pressed concurrently
- Extensibility: adding VIP, freight, or express elevators should not require rewriting existing classes
- O(1) or O(log n) elevator selection at the dispatcher level
- State transitions must be validated: no illegal transitions (e.g., MOVING while doors OPEN)
Out of Scope:
- UI rendering or physical motor control
- Persistence or database
- Emergency fire-service mode (discussed in Extensibility)
- Predictive scheduling or machine learning
Example Inputs and Outputs
Scenario 1: Simple pickup and drop-off
- Input: Person on floor 7 presses UP. Elevator 1 is idle on floor 5.
- Expected: Dispatcher assigns elevator 1. It moves up to floor 7, opens doors. Passenger enters, presses floor 12. Elevator moves to floor 12, opens doors, passenger exits.
- Why: Validates the basic external-request-to-internal-request flow.
Scenario 2: Direction commitment (SCAN)
- Input: Elevator 2 is on floor 10 moving UP with stops at 15 and 20. A new external request arrives: floor 6, UP.
- Expected: Elevator 2 does NOT reverse. It serves 15 and 20 first, then reverses direction and serves floor 6.
- Why: Validates SCAN algorithm; the car commits to its current direction.
Scenario 3: Overweight rejection
- Input: Elevator 3 is on floor 1 with 900kg loaded (capacity 1000kg). A 150kg passenger tries to board.
- Expected: The elevator signals an overweight alarm. Doors remain open. The car does not move until weight drops below 1000kg.
- Why: Validates capacity enforcement as a safety invariant.
Try It Yourself
Try it yourself
Before reading the solution, spend 20 minutes sketching your own class diagram. Focus on separating the dispatcher (which car?) from the elevator controller (how does the car move?) and the door state machine (when can doors open?). These three concerns are the core decomposition challenge.
Step 1: Identify Core Entities
Start by asking: what are the main "things" in this problem? Look for nouns in your requirements. Every noun that has its own lifecycle or responsibility becomes a candidate class.
A common mistake in elevator design is cramming everything into a single Elevator class that handles movement, doors, requests, and dispatching. That class balloons to 500+ lines and becomes untestable. Good design means each class has a single, clear job.
| Entity | Responsibility | Key attributes |
|---|---|---|
| ElevatorSystem | Top-level orchestrator. Holds all elevators and the dispatcher. Routes external requests. | elevators, dispatcher, floors |
| Elevator | A single car. Manages its own state, request queue, direction, and current floor. | id, currentFloor, direction, state, requestQueue, door, capacity |
| Request | A value object combining a target floor and direction (for external) or just a floor (for internal). | floor, direction, type |
| Door | The door state machine. Tracks CLOSED/OPENING/OPEN/CLOSING transitions. | state |
| Dispatcher | Picks which elevator handles an external request. Delegates to a strategy. | strategy |
| DispatchStrategy | Interface for elevator selection algorithms. Implementations: NearestIdle, ZoneBased, ScanAware. | (interface) |
| Floor | Represents a building floor. Holds the UP and DOWN call buttons. | floorNumber, upButton, downButton |
| Button | An input device. Can be an external floor button (with direction) or an internal car button (destination). | active, floor, direction |
Notice we separated Dispatcher from ElevatorSystem. The system owns the elevators and forwards requests, but the "which car?" decision lives in the Dispatcher. This separation means swapping scheduling algorithms requires changing one strategy object, not the entire system.
The Elevator itself does not decide which requests to accept. It only processes its own queue. The Dispatcher makes assignment decisions. This mirrors real buildings where a central controller dispatches, and each car just follows its queue.
Step 2: Define Relationships and Class Design
Class Diagram
Class Interface Derivation
ElevatorSystem
The top-level orchestrator. It receives external requests from floor buttons, delegates to the Dispatcher for car selection, and forwards the request to the chosen Elevator.
Deriving state from requirements:
| Requirement | What ElevatorSystem must track |
|---|---|
| "Multiple elevators, configurable" | List of Elevator instances |
| "Dispatcher assigns best elevator" | A Dispatcher with a pluggable strategy |
| "N floors" | Total floor count for validation |
Deriving methods from needs:
| Need from requirements | Method |
|---|---|
| "Person presses UP on floor 12" | requestElevator(floor, direction) |
| "View all elevator positions" | getStatus() |
Elevator
The core state machine. Tracks current floor, committed direction, pending stops in each direction, door state, and weight.
Deriving state from requirements:
| Requirement | What Elevator must track |
|---|---|
| "Serve all requests in current direction" | Two sorted sets: upStops, downStops |
| "Door must be closed before moving" | A Door object with its own state machine |
| "Weight capacity enforcement" | currentWeight, maxCapacity |
| "Car is moving up / down / idle" | ElevatorState enum, Direction enum |
I use two separate TreeSets for up-stops and down-stops rather than a single priority queue. This makes the SCAN algorithm trivial: when moving up, poll the lowest floor from upStops; when moving down, poll the highest from downStops. When one set empties, reverse direction and switch to the other set.
Deriving methods from needs:
| Need from requirements | Method |
|---|---|
| "Add floor to request queue" | addRequest(floor) |
| "Move one floor in current direction" | move() |
| "Open doors at destination" | openDoors() |
| "Close doors before moving" | closeDoors() |
| "Check weight before moving" | isOverloaded() |
Door
A standalone state machine with four states and strict transition rules.
Valid transitions:
| From | To | Trigger |
|---|---|---|
| CLOSED | OPENING | open() called |
| OPENING | OPEN | timer completes |
| OPEN | CLOSING | close() called |
| CLOSING | CLOSED | timer completes |
| CLOSING | OPENING | obstruction or reopen request |
The CLOSING-to-OPENING reversal is the interesting edge case. In real elevators, if a sensor detects an obstruction while doors are closing, they reverse immediately. We model this as a valid transition.
Door State Machine
Key Relationship Decisions
ElevatorSystem owns Elevators (composition): Elevators do not exist outside the system. The system creates them at construction time and manages their lifecycle.
Dispatcher uses DispatchStrategy (delegation): The dispatcher holds a reference to a strategy interface. Swapping algorithms at runtime is a one-line call: dispatcher.setStrategy(new ZoneBasedStrategy()).
Elevator owns Door (composition): Each car has exactly one door. The door does not exist independently. But it has its own state machine, so it is a separate class.
Step 3: Choose Design Patterns
Pattern: Strategy, for Dispatch Algorithm Selection
The signal: "Support multiple strategies... nearest idle, zone-based, SCAN-aware." Whenever the requirements say "multiple algorithms for the same action, swappable at runtime," that is the Strategy pattern.
Why Strategy over alternatives: We could use a giant switch statement inside the Dispatcher. But every new algorithm would modify that switch, violating the Open/Closed Principle. With Strategy, adding a new algorithm means adding a class, not editing existing code.
How it maps to our entities:
- Interface:
DispatchStrategy - Concrete implementations:
NearestIdleStrategy,ScanAwareStrategy,ZoneBasedStrategy - Context:
Dispatcherholds a reference to the active strategy
Pattern: State, for Door State Machine
The signal: "Doors transition through CLOSED, OPENING, OPEN, CLOSING" with complex transition rules and the CLOSING-to-OPENING reversal. Whenever an object's behavior changes based on its internal state, that is the State pattern.
Why State over a simple enum: A plain enum with switch statements works for trivial state machines. But the door has conditional transitions (CLOSING can go to OPENING only on obstruction) and state-specific behavior (cannot open while elevator is moving). The State pattern encapsulates each state's rules inside the state object itself.
For a 45-minute interview, I would mention the State pattern as the ideal approach but implement a simpler enum-based version with validated transitions. The State pattern is the "if the interviewer asks for more" answer.
Pattern Selection: Scheduling Algorithms
This is the core design decision. The scheduling algorithm determines how efficiently the system serves requests. Let me walk through the progression.
Interview tip: name the algorithm
Say "I will use the LOOK algorithm, which is a variant of SCAN that reverses at the last pending request instead of the shaft boundary." Naming the algorithm signals that you have studied scheduling theory, not just guessing.
Step 4: Build the Solution
Pseudocode for Key Methods
move(): The Core Elevator Step
This is the method that runs on every tick of the simulation. It moves the car one floor in its current direction, checks for pending stops, and handles door operations.
Core logic (happy path):
- If doors are not closed, do nothing (safety invariant)
- If the current floor is a pending stop, remove it and open doors
- Otherwise, move one floor in the current direction
- If no more stops in the current direction, reverse or go idle
Edge cases (reject before touching state):
- Elevator is overloaded: refuse to move, signal alarm
- No pending stops in either direction: go IDLE, do not move
- Door is in OPENING or CLOSING transition: wait for completion
move():
if door.isNotClosed():
return // safety: never move with doors not fully closed
if isOverloaded():
signalOverweightAlarm()
return
if currentDirectionStops().contains(currentFloor):
currentDirectionStops().remove(currentFloor)
door.open()
return
if currentDirectionStops().isEmpty():
if oppositeDirectionStops().isEmpty():
state = IDLE
direction = IDLE
return
else:
reverseDirection()
// Move one floor in the current direction
if direction == UP:
currentFloor++
state = MOVING_UP
else:
currentFloor--
state = MOVING_DOWN
dispatch(): Choosing the Best Elevator
dispatch(floor, direction, elevators):
elevator = strategy.selectElevator(floor, direction, elevators)
if elevator != null:
elevator.addRequest(floor)
return elevator
The dispatcher is intentionally thin. All intelligence lives in the strategy. This makes testing trivial: inject a mock strategy, verify that dispatch calls it.
addRequest(): Direction-Aware Queuing
addRequest(floor):
if floor > currentFloor:
upStops.add(floor)
else if floor < currentFloor:
downStops.add(floor)
else:
// Already at this floor, just open doors
door.open()
// If idle, set direction toward the new request
if state == IDLE:
direction = floor > currentFloor ? UP : DOWN
state = direction == UP ? MOVING_UP : MOVING_DOWN
Full Implementation
// Represents the three possible movement directions for an elevator.
// IDLE means the car has no pending requests and is stationary.
public enum Direction {
UP, DOWN, IDLE
}
Architecture note: The files split into three layers. model/ holds pure data (enums, records) and the Elevator/Door state machines. strategy/ holds the pluggable dispatch algorithms. service/ holds the orchestration layer. SOLID principles in action: SRP (each class has one job), OCP (new strategies require no changes to existing classes), DIP (Dispatcher depends on the DispatchStrategy interface, not concrete implementations).
Step 5: Trace a Scenario
Let's trace Scenario 2 from the examples: an elevator moving up with existing stops, and a new request behind it.
Setup: Elevator 0 is on floor 10, moving UP, with stops at floors 15 and 20. A new external request arrives: floor 6, direction UP.
Step-by-step walkthrough:
FloorPanelcallssystem.requestElevator(6, UP)when a passenger on floor 6 presses UPElevatorSystemdelegates todispatcher.dispatch(6, UP, elevators)ScanAwareStrategy.selectElevator()scores each elevator:- Elevator 0 is on floor 10 heading UP. Floor 6 is below it, so it is not "on the way." Score = 4 + 40 = 44 (heavy penalty)
- Elevator 1 is idle on floor 1. Score = 5 (pure distance)
- Strategy returns Elevator 1 (score 5 beats 44). Elevator 0 is not interrupted.
- Elevator 0 continues its upward sweep: floors 11, 12, 13, 14, 15 (stop, open doors, close doors), 16, 17, 18, 19, 20 (stop, open doors)
- After floor 20,
upStopsis empty,downStopsis empty, so Elevator 0 goes IDLE
The key insight: SCAN-aware dispatch does NOT pull an already-committed elevator off its route. It scores wrong-direction assignments with a heavy penalty so idle or compatible elevators are preferred.
Extensibility
1. "How would you add VIP or express elevators?"
"Today all elevators are identical because the requirements say so. If we needed VIP elevators, I would create a sealed interface ElevatorType with implementations like StandardElevator, VIPElevator, and FreightElevator. The Dispatcher's strategy would filter the elevator list by type before scoring. The Elevator class itself does not change at all because the type distinction is a dispatch concern, not a movement concern."
// New: elevator type hierarchy
public sealed interface ElevatorType
permits Standard, VIP, Freight {}
public record Standard() implements ElevatorType {}
public record VIP() implements ElevatorType {}
// DispatchStrategy receives filtered list
strategy.selectElevator(floor, dir,
elevators.stream()
.filter(e -> e.getType() instanceof VIP)
.toList());
OCP in action: existing strategies and the Elevator class remain untouched.
2. "How would you add emergency mode?"
"I would add a system-level EmergencyMode flag. When activated, all elevators move to the ground floor, doors open, and the Dispatcher rejects all new requests. The ElevatorSystem.requestElevator() method checks the flag before dispatching. Each Elevator's move() method checks it before processing its queue. This is a cross-cutting concern that layers on top of the existing design."
3. "How would you add predictive scheduling?"
"Predictive scheduling uses historical data (e.g., 'from 8-9 AM, 80% of requests go from lobby to floors 20-30'). I would create a PredictiveStrategy that extends DispatchStrategy and pre-positions idle elevators near predicted hotspots. During peak hours, the system switches to this strategy via setStrategy(). No other class changes because the strategy interface already encapsulates the full selection logic."
4. "What about energy-saving mode?"
"Park idle elevators at evenly spaced floors instead of wherever they last stopped. This is a background task: a RepositioningService periodically checks idle elevators and sends them to optimal positions. It calls elevator.addRequest(optimalFloor) just like any other request. The Elevator class has no idea it is being repositioned; it just processes the stop."
Common Interview Mistakes
| Mistake | Why it's wrong | What to do instead |
|---|---|---|
| Putting dispatch logic inside Elevator | Violates SRP; the car should not decide which requests to accept | Separate Dispatcher with Strategy pattern |
| No direction commitment (serving requests in any order) | Causes thrashing; the car oscillates between floors | Implement SCAN: serve all stops in current direction first |
| Modeling Door as a boolean (open/closed) | Misses the OPENING/CLOSING transition states and the reversal edge case | Use a four-state machine with validated transitions |
| Ignoring capacity until asked | Makes the interviewer think you only handle the happy path | Mention weight limit in requirements and enforce it in move() |
| One giant Elevator class with 20+ methods | Untestable, hard to extend, violates SRP | Split into Elevator (movement), Door (state machine), Dispatcher (selection) |
| Hardcoding the scheduling algorithm | Cannot adapt to different building profiles or runtime changes | Use Strategy pattern; mention SCAN/LOOK by name |
My recommendation for interviews: draw the class diagram first, then talk through the dispatch algorithm verbally before writing code. Interviewers care more about your decomposition and algorithm choice than perfect syntax.
A second tip: always mention the SCAN algorithm by name. It is the same concept as disk scheduling in operating systems, and naming it signals cross-domain knowledge that impresses interviewers.
Test Your Understanding
Interview Cheat Sheet
- Start with entities: ElevatorSystem, Elevator, Door, Dispatcher, Request. Draw the class diagram first.
- Name the algorithm: "I will use LOOK, a variant of SCAN that reverses at the last pending request."
- Two request types: External (floor + direction, goes to Dispatcher) and internal (destination floor, goes directly to the car).
- Strategy pattern for dispatch: NearestIdle for simple buildings, SCAN-aware for high-traffic, ZoneBased for skyscrapers.
- Door is a state machine: CLOSED, OPENING, OPEN, CLOSING. The car never moves unless doors are CLOSED.
- Two TreeSets for SCAN: upStops and downStops. Serve one set, then reverse and serve the other.
- Capacity check in move(): Overloaded cars refuse to move. Always mention this proactively.
Quick Recap
- Decompose the system into three concerns: dispatching (which car?), movement (how does the car travel?), and door management (when can doors open?). Each gets its own class.
- The Strategy pattern fits naturally when requirements mention "multiple algorithms, swappable at runtime." Here it powers the Dispatcher.
- SCAN/LOOK algorithms prevent direction thrashing by committing the car to its current direction until all stops are served. Name the algorithm in your interview.
- Two TreeSets (upStops, downStops) make direction-based queuing trivial: serve one set, flip to the other on reversal.
- The Door state machine is the most commonly skipped entity. Four states with validated transitions show the interviewer you think about safety invariants.
- Capacity enforcement is a single check in
move(), but mentioning it proactively signals mature engineering thinking. - In interviews, draw the class diagram and explain the dispatch algorithm before writing any code. The decomposition and algorithm choice matter more than syntax.