Loading learning content...
We've covered every aspect of the elevator system: requirements analysis, entity modeling, state machines, scheduling algorithms, and design pattern integration. Now we synthesize everything into a complete, coherent design.
This page serves multiple purposes:
By the end of this page, you will be able to:
• Present a complete elevator system design with confidence • Walk through complex scenarios demonstrating system behavior • Identify and articulate extension points for new requirements • Handle interviewer questions about design decisions • Apply this design methodology to other LLD problems
The final class diagram shows all entities, their relationships, and the patterns that connect them. This is what you would draw on a whiteboard in an interview or present in a design review.
Design Overview:
Key Design Decisions Visible in the Diagram:
The best way to validate a design is to walk through realistic scenarios. Let's trace a complete flow from button press to destination arrival.
Scenario: Rush Hour Multi-Passenger Journey
Setting: 20-floor building, 4 elevators, morning rush hour (up-peak traffic)
Passenger P1 at Floor 5 presses UP, wants to go to Floor 18. Passenger P2 at Floor 7 presses UP (shortly after), wants Floor 14.
| Step | Action | Component Involved | Result |
|---|---|---|---|
| 1 | P1 presses UP at Floor 5 | Floor → Controller | Hall call created: (5, UP) |
| 2 | Controller receives hall call | Controller → Strategy (LOOK) | Evaluates: A (idle, 4 floors away), B (UP, past 5), C (DOWN) |
| 3 | Strategy selects Elevator A | LOOKScheduler.selectElevator() | A is closest idle elevator |
| 4 | Request assigned to A | Controller | A.addDestination(request) |
| 5 | A's state handles request | IdleState.onRequestReceived() | startMotor(UP), setState(MovingUpState) |
| 6 | Observers notified | Observer pattern | Display updates, logs event, metrics start timer |
| 7 | A passes Floor 2, 3, 4 | MovingUpState.onFloorReached() | Not in queue, continue moving |
| 8 | A reaches Floor 5 | MovingUpState.onFloorReached() | In queue! stopMotor(), setState(StoppedAtFloorState) |
| 9 | Doors open | DoorStateMachine | State: OPENING → OPEN, start dwell timer |
| 10 | P1 enters, presses 18 | Controller.handleCabCall() | Cab call (18) added to A's queue |
| 11 | P2 presses UP at Floor 7 | Floor → Controller | Second hall call: (7, UP) |
| 12 | Strategy evaluates for P2 | LOOKScheduler | A is at 5, going UP, will pass 7 → select A |
| 13 | Floor 7 added to A's queue | Strategy.orderDestinations() | Queue reordered: [7, 18] (LOOK order) |
| 14 | Dwell timer expires, doors close | DoorStateMachine | State: CLOSING → CLOSED |
| 15 | StoppedAtFloorState.onDoorsClosed() | State pattern | Has destinations, continue UP |
| 16 | A moves to Floor 7 | MovingUpState | Stops for P2's hall call |
| 17 | P2 enters, presses 14 | Controller.handleCabCall() | Queue: [14, 18] (14 is below 18, served first) |
| 18 | A continues to 14 | MovingUpState | Arrives, opens doors, P2 exits |
| 19 | A continues to 18 | MovingUpState | Arrives, opens doors, P1 exits |
| 20 | No more destinations | StoppedAtFloorState.onDoorsClosed() | setState(IdleState) |
Walking through a scenario like this in an interview demonstrates mastery. You're showing not just the static design but how it behaves dynamically. Point to the class diagram as you describe each step: 'Here the Controller uses the Strategy to select the elevator, which triggers a state transition in the Elevator, which notifies Observers.'
Let's see the complete, integrated implementation showing how all pieces fit together. This is production-ready code structure.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
// ==========================================// DOMAIN LAYER - Core Business Logic// ========================================== // Value Objectsenum Direction { UP = 'UP', DOWN = 'DOWN', IDLE = 'IDLE' }enum DoorState { OPEN = 'OPEN', OPENING = 'OPENING', CLOSED = 'CLOSED', CLOSING = 'CLOSING' }enum RequestType { HALL_CALL = 'HALL_CALL', CAB_CALL = 'CAB_CALL' }enum RequestStatus { PENDING = 'PENDING', ASSIGNED = 'ASSIGNED', SATISFIED = 'SATISFIED' } // Entitiesclass Floor { constructor( public readonly floorNumber: number, public readonly name: string = `Floor ${floorNumber}` ) {} equals(other: Floor): boolean { return this.floorNumber === other.floorNumber; }} class Request { private static nextId = 1; readonly id: number = Request.nextId++; readonly createdAt = new Date(); status: RequestStatus = RequestStatus.PENDING; assignedElevator?: Elevator; constructor( readonly type: RequestType, readonly floor: Floor, readonly direction: Direction | null ) {} static hallCall(floor: Floor, direction: Direction): Request { return new Request(RequestType.HALL_CALL, floor, direction); } static cabCall(floor: Floor): Request { return new Request(RequestType.CAB_CALL, floor, null); }} // State Interfaceinterface IElevatorState { getName(): string; onRequestReceived(ctx: Elevator, req: Request): void; onFloorReached(ctx: Elevator, floor: Floor): void; onDoorsClosed(ctx: Elevator): void;} // Concrete Statesclass IdleState implements IElevatorState { getName() { return 'IDLE'; } onRequestReceived(ctx: Elevator, req: Request): void { ctx.addToQueue(req); const current = ctx.currentFloor.floorNumber; const target = req.floor.floorNumber; if (target > current) { ctx.startMotor(Direction.UP); ctx.setState(new MovingUpState()); } else if (target < current) { ctx.startMotor(Direction.DOWN); ctx.setState(new MovingDownState()); } else { ctx.openDoors(); ctx.setState(new StoppedAtFloorState()); } } onFloorReached(ctx: Elevator, floor: Floor): void { } onDoorsClosed(ctx: Elevator): void { }} class MovingUpState implements IElevatorState { getName() { return 'MOVING_UP'; } onRequestReceived(ctx: Elevator, req: Request): void { ctx.addToQueue(req); } onFloorReached(ctx: Elevator, floor: Floor): void { ctx.currentFloor = floor; if (ctx.shouldStopAt(floor)) { ctx.stopMotor(); ctx.openDoors(); ctx.setState(new StoppedAtFloorState()); } } onDoorsClosed(ctx: Elevator): void { }} class MovingDownState implements IElevatorState { getName() { return 'MOVING_DOWN'; } onRequestReceived(ctx: Elevator, req: Request): void { ctx.addToQueue(req); } onFloorReached(ctx: Elevator, floor: Floor): void { ctx.currentFloor = floor; if (ctx.shouldStopAt(floor)) { ctx.stopMotor(); ctx.openDoors(); ctx.setState(new StoppedAtFloorState()); } } onDoorsClosed(ctx: Elevator): void { }} class StoppedAtFloorState implements IElevatorState { getName() { return 'STOPPED'; } onRequestReceived(ctx: Elevator, req: Request): void { ctx.addToQueue(req); } onFloorReached(ctx: Elevator, floor: Floor): void { } onDoorsClosed(ctx: Elevator): void { ctx.satisfyCurrentFloorRequests(); if (!ctx.hasDestinations()) { ctx.setState(new IdleState()); return; } const next = ctx.peekNextDestination()!; const current = ctx.currentFloor.floorNumber; const target = next.floor.floorNumber; if (target > current) { ctx.startMotor(Direction.UP); ctx.setState(new MovingUpState()); } else { ctx.startMotor(Direction.DOWN); ctx.setState(new MovingDownState()); } }} // ==========================================// APPLICATION LAYER - Orchestration// ========================================== // Observer Patterninterface ElevatorObserver { onEvent(type: string, elevator: Elevator, data?: any): void;} // Elevator Entity with State and Observerclass Elevator { private state: IElevatorState = new IdleState(); private queue: Request[] = []; private observers: Set<ElevatorObserver> = new Set(); direction: Direction = Direction.IDLE; currentFloor: Floor; constructor( readonly id: number, initialFloor: Floor, private scheduler: SchedulingStrategy ) { this.currentFloor = initialFloor; } // State management setState(newState: IElevatorState): void { this.state = newState; this.notify('STATE_CHANGED', { state: newState.getName() }); } getState(): IElevatorState { return this.state; } // Request handling - delegate to state handleRequest(req: Request): void { this.state.onRequestReceived(this, req); } handleFloorArrival(floor: Floor): void { this.state.onFloorReached(this, floor); } handleDoorsClosed(): void { this.state.onDoorsClosed(this); } // Queue management addToQueue(req: Request): void { this.queue.push(req); this.reorderQueue(); } private reorderQueue(): void { this.queue = this.scheduler.orderDestinations( this.currentFloor.floorNumber, this.direction, this.queue ); } shouldStopAt(floor: Floor): boolean { return this.queue.some(r => r.floor.equals(floor)); } hasDestinations(): boolean { return this.queue.length > 0; } peekNextDestination(): Request | undefined { return this.queue[0]; } satisfyCurrentFloorRequests(): void { this.queue = this.queue.filter(r => !r.floor.equals(this.currentFloor)); } // Motor control (abstracted) startMotor(dir: Direction): void { this.direction = dir; this.notify('MOTOR_STARTED', { direction: dir }); } stopMotor(): void { this.notify('MOTOR_STOPPED'); } openDoors(): void { this.notify('DOORS_OPENING'); // Simulate door operation setTimeout(() => this.notify('DOORS_OPEN'), 1500); } // Observer pattern subscribe(obs: ElevatorObserver): void { this.observers.add(obs); } unsubscribe(obs: ElevatorObserver): void { this.observers.delete(obs); } private notify(type: string, data?: any): void { this.observers.forEach(o => o.onEvent(type, this, data)); }} // Strategy Pattern - Schedulinginterface SchedulingStrategy { selectElevator(req: Request, elevators: Elevator[]): Elevator | null; orderDestinations(current: number, dir: Direction, reqs: Request[]): Request[];} class LOOKScheduler implements SchedulingStrategy { selectElevator(req: Request, elevators: Elevator[]): Elevator | null { const available = elevators.filter(e => e.getState().getName() !== 'MAINTENANCE' ); if (available.length === 0) return null; return available.reduce((best, curr) => { const bestScore = this.score(best, req); const currScore = this.score(curr, req); return currScore < bestScore ? curr : best; }); } private score(e: Elevator, req: Request): number { const current = e.currentFloor.floorNumber; const target = req.floor.floorNumber; const dir = e.direction; if (dir === Direction.IDLE) return Math.abs(target - current); const inDirection = (dir === Direction.UP && target > current) || (dir === Direction.DOWN && target < current); if (inDirection) return Math.abs(target - current); return 40 + Math.abs(target - current); // Penalty for wrong direction } orderDestinations(current: number, dir: Direction, reqs: Request[]): Request[] { const above = reqs.filter(r => r.floor.floorNumber > current) .sort((a, b) => a.floor.floorNumber - b.floor.floorNumber); const below = reqs.filter(r => r.floor.floorNumber < current) .sort((a, b) => b.floor.floorNumber - a.floor.floorNumber); const same = reqs.filter(r => r.floor.floorNumber === current); return dir === Direction.DOWN ? [...same, ...below, ...above] : [...same, ...above, ...below]; }} // Controller - Orchestrates everythingclass ElevatorController { constructor( private elevators: Elevator[], private strategy: SchedulingStrategy ) {} handleHallCall(floor: Floor, direction: Direction): void { const request = Request.hallCall(floor, direction); const elevator = this.strategy.selectElevator(request, this.elevators); if (elevator) { request.assignedElevator = elevator; request.status = RequestStatus.ASSIGNED; elevator.handleRequest(request); } } handleCabCall(elevatorId: number, floor: Floor): void { const elevator = this.elevators.find(e => e.id === elevatorId); if (elevator) { const request = Request.cabCall(floor); request.assignedElevator = elevator; elevator.handleRequest(request); } } setStrategy(strategy: SchedulingStrategy): void { this.strategy = strategy; }}A mature design anticipates future requirements. Here's how our elevator system can be extended without modifying existing code—demonstrating the Open/Closed Principle in action.
| Future Requirement | Extension Approach | Existing Code Modified? |
|---|---|---|
| Destination Dispatch (enter floor before car) | Create DestinationDispatchController extending ElevatorController | No - new class |
| VIP/Priority Elevators | Create PrioritySchedulingStrategy implementing SchedulingStrategy | No - new strategy |
| Weight Sensors | Add WeightObserver, modify shouldStopAt() to check capacity | Minimal - add observer |
| Voice Announcements | Create VoiceAnnouncementObserver subscribing to events | No - new observer |
| Mobile App Integration | Create WebSocketObserver pushing events to connected clients | No - new observer |
| Energy Saving Mode | Create EnergySavingState, add transitions from IdleState | Minimal - add state |
| Fire Alarm Mode | Create EmergencyState with special behavior, triggered by external event | Minimal - add state and trigger |
| Express Elevators | Configure floor list per elevator, modify canServeFloor() | Minimal - configuration |
Example: Adding VIP Priority Service
Let's demonstrate extending the system for VIP priority requests:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
// Extension: VIP Priority Scheduling// No modification to existing LOOK scheduler needed enum RequestPriority { NORMAL = 0, HIGH = 1, VIP = 2, EMERGENCY = 3} // Extended Request with priorityclass PriorityRequest extends Request { constructor( type: RequestType, floor: Floor, direction: Direction | null, public priority: RequestPriority = RequestPriority.NORMAL ) { super(type, floor, direction); }} // New strategy that considers priorityclass PriorityLOOKScheduler extends LOOKScheduler { orderDestinations( current: number, dir: Direction, reqs: Request[] ): Request[] { // First, group by priority const byPriority = new Map<RequestPriority, Request[]>(); for (const request of reqs) { const priority = (request as PriorityRequest).priority ?? RequestPriority.NORMAL; if (!byPriority.has(priority)) { byPriority.set(priority, []); } byPriority.get(priority)!.push(request); } // Process highest priority first const result: Request[] = []; const priorities = [ RequestPriority.EMERGENCY, RequestPriority.VIP, RequestPriority.HIGH, RequestPriority.NORMAL ]; for (const priority of priorities) { const group = byPriority.get(priority) ?? []; // Within priority, use LOOK ordering const ordered = super.orderDestinations(current, dir, group); result.push(...ordered); } return result; }} // Usage: swap strategy without changing controllercontroller.setStrategy(new PriorityLOOKScheduler());In interviews, mentioning extension points proactively impresses interviewers. Say: 'The design is extensible—for VIP elevators, I'd add a PrioritySchedulingStrategy. For fire alarms, I'd add an EmergencyState. Neither requires changing tested, working code.'
Every design involves trade-offs. Understanding and articulating these is a hallmark of senior engineering. Here are the key decisions in our elevator system and their implications.
| Decision | Alternative Considered | Our Choice | Rationale |
|---|---|---|---|
| State Pattern for elevator modes | Switch statements | State Pattern | More classes but much better extensibility and testability |
| Strategy Pattern for scheduling | Hard-coded algorithm | Strategy Pattern | Slight overhead but enables runtime algorithm switching |
| Observer for notifications | Direct method calls | Observer Pattern | Decouples components; easy to add new observers |
| Unified Request class | Separate HallCall/CabCall | Single class with type | Simpler queue handling; type field distinguishes when needed |
| Floor as entity | Floor as integer | Entity | Slightly heavier but supports rich behavior and extensions |
| Controller as mediator | Elevators coordinate directly | Central controller | Simpler coordination logic; single point of truth for dispatch |
| In-memory queue | Persistent queue | In-memory | Sufficient for interview scope; would persist for production |
Key Trade-off: Complexity vs Extensibility
Our design uses three patterns (State, Strategy, Observer) where simpler implementations would work. Is this over-engineering?
No, for these reasons:
However, for a 2-floor, 1-elevator system, this would be over-engineering. Patterns are tools, applied judiciously.
In interviews, demonstrating restraint is as important as demonstrating knowledge. If asked about a simple 3-floor elevator, acknowledge: 'For this scope, I'd start with a simpler implementation. But if the interviewer expects growth to multiple elevators and scheduling complexity, patterns become worthwhile.'
An LLD interview typically runs 45-60 minutes. Here's how to present our elevator design effectively.
Recommended Time Allocation:
Common Interviewer Questions and Strong Answers:
| Question | Strong Answer |
|---|---|
| Why State pattern over conditionals? | 'With N states and M events, conditionals give O(N×M) complexity. State pattern keeps each state's logic isolated, testable, and makes adding states trivial.' |
| Why didn't you use inheritance for Request types? | 'Composition over inheritance—a single class with type discriminator is simpler, and the behaviors aren't different enough to warrant separate hierarchies.' |
| How does the system handle concurrent requests? | 'The Controller acts as serialization point. For true concurrency, I'd add thread-safe queues and consider actor model for elevator entities.' |
| What about testing? | 'Each state is independently testable. Strategies are tested with mock elevators. Integration tests simulate button press sequences.' |
| How would you handle 100 elevators? | 'Zone-based dispatch, potentially multiple controllers with load balancing, and async event handling to prevent bottlenecks.' |
Congratulations! You've completed a comprehensive, production-grade design of an elevator control system. Let's summarize what makes this design exemplary:
The Methodology You've Learned:
This case study demonstrates a repeatable methodology for any LLD problem:
Apply this methodology to parking lots, chess games, ride-sharing, and any other LLD problem—the patterns and approach transfer directly.
You now possess a world-class understanding of elevator system design—sufficient for both interviews and real-world implementation. Remember: the goal isn't to memorize this specific design, but to internalize the methodology and patterns so you can apply them to any complex domain.