Loading learning content...
If entities are the nouns of our system, state machines are the verbs—they define how entities behave and transition over time. For an elevator system, state management is particularly critical because:
In this page, we design comprehensive state machines for elevator behavior and implement them using the State Pattern, one of the most elegant solutions for managing complex state transitions in object-oriented systems.
By the end of this page, you will be able to:
• Design finite state machines that model real-world behavior faithfully • Identify states, events, transitions, and guards in a domain • Implement the State Pattern with context, state interface, and concrete states • Handle edge cases through guard conditions and error states • Test state machines systematically
A Finite State Machine (FSM) is a mathematical model of computation with:
Why FSMs for Elevators?
Elevators are perfect candidates for FSM modeling because:
Components of Our FSM:
| Component | Description | Examples |
|---|---|---|
| States | Discrete conditions of the elevator | IDLE, MOVING_UP, MOVING_DOWN, STOPPED, DOORS_OPENING, DOORS_OPEN, DOORS_CLOSING, MAINTENANCE |
| Events | Triggers that can cause transitions | BUTTON_PRESSED, FLOOR_REACHED, DOOR_OPEN_COMPLETE, DOOR_CLOSE_COMPLETE, TIMER_EXPIRED, EMERGENCY |
| Transitions | State changes in response to events | (IDLE, BUTTON_PRESSED) → MOVING_UP, (MOVING_UP, FLOOR_REACHED) → DOORS_OPENING |
| Guards | Conditions that must be true for transition | Doors not obstructed, Floor is valid destination |
| Actions | Operations executed during transitions | Start motor, Stop motor, Notify controller |
State Explosion Problem:
One challenge is state explosion—the combinatorial growth of states when modeling multiple aspects. An elevator has:
Naively combining these gives 4 × 4 × 3 = 48 states! We'll use hierarchical state machines and orthogonal states to manage complexity.
UML statecharts (Harel statecharts) extend basic FSMs with hierarchical states and parallel regions. An elevator can be in (MOVING.UP, DOORS.CLOSED) simultaneously—two orthogonal regions updated independently. This keeps complexity manageable.
Let's design the primary state machine governing elevator movement. This focuses on the vertical position and direction of the elevator car.
Movement States:
Movement Events:
State Transition Diagram:
Transition Rules in Detail:
IDLE → MOVING_UP:
MOVING_UP → STOPPED_AT_FLOOR:
MOVING_UP → MOVING_UP (self-transition):
This is a critical distinction—elevators don't stop at every floor, only at destinations.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
// Movement state enumerationenum MovementState { IDLE = 'IDLE', MOVING_UP = 'MOVING_UP', MOVING_DOWN = 'MOVING_DOWN', STOPPED_AT_FLOOR = 'STOPPED_AT_FLOOR', MAINTENANCE = 'MAINTENANCE'} // Transition table approach (data-driven)interface Transition { from: MovementState; event: string; guard?: (context: ElevatorContext) => boolean; to: MovementState; action?: (context: ElevatorContext) => void;} const movementTransitions: Transition[] = [ // From IDLE { from: MovementState.IDLE, event: 'REQUEST_UP', guard: (ctx) => ctx.doorsAreClosed() && ctx.hasRequestAbove(), to: MovementState.MOVING_UP, action: (ctx) => ctx.startMotorUp() }, { from: MovementState.IDLE, event: 'REQUEST_DOWN', guard: (ctx) => ctx.doorsAreClosed() && ctx.hasRequestBelow(), to: MovementState.MOVING_DOWN, action: (ctx) => ctx.startMotorDown() }, { from: MovementState.IDLE, event: 'ENTER_MAINTENANCE', to: MovementState.MAINTENANCE, action: (ctx) => ctx.disableButtons() }, // From MOVING_UP { from: MovementState.MOVING_UP, event: 'FLOOR_REACHED', guard: (ctx) => ctx.shouldStopAtCurrentFloor(), to: MovementState.STOPPED_AT_FLOOR, action: (ctx) => { ctx.stopMotor(); ctx.triggerDoorOpen(); } }, { from: MovementState.MOVING_UP, event: 'FLOOR_REACHED', guard: (ctx) => !ctx.shouldStopAtCurrentFloor(), to: MovementState.MOVING_UP, // Self-transition action: (ctx) => ctx.updateCurrentFloor() }, // From MOVING_DOWN { from: MovementState.MOVING_DOWN, event: 'FLOOR_REACHED', guard: (ctx) => ctx.shouldStopAtCurrentFloor(), to: MovementState.STOPPED_AT_FLOOR, action: (ctx) => { ctx.stopMotor(); ctx.triggerDoorOpen(); } }, // From STOPPED_AT_FLOOR { from: MovementState.STOPPED_AT_FLOOR, event: 'DOORS_CLOSED', guard: (ctx) => ctx.hasMoreRequestsInCurrentDirection(), to: MovementState.MOVING_UP, // or MOVING_DOWN based on context action: (ctx) => ctx.continueInDirection() }, { from: MovementState.STOPPED_AT_FLOOR, event: 'DOORS_CLOSED', guard: (ctx) => !ctx.hasMoreRequests(), to: MovementState.IDLE, }, // From MAINTENANCE { from: MovementState.MAINTENANCE, event: 'EXIT_MAINTENANCE', to: MovementState.IDLE, action: (ctx) => ctx.enableButtons() },];The door state machine operates as an orthogonal region to movement—doors have their own states independent of (but coordinated with) movement.
Door States:
Door Events:
Critical Safety Constraint:
The door and movement state machines must coordinate. The fundamental safety invariant:
Movement is only allowed when doors are CLOSED.
This is enforced by making the doorsAreClosed() guard a precondition for any movement transition.
Door Dwell Time Logic:
When doors reach OPEN state:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
enum DoorState { CLOSED = 'CLOSED', OPENING = 'OPENING', OPEN = 'OPEN', CLOSING = 'CLOSING'} class DoorStateMachine { private state: DoorState = DoorState.CLOSED; private dwellTimer: NodeJS.Timeout | null = null; private readonly dwellDuration: number = 3000; // 3 seconds constructor( private readonly onStateChange: (newState: DoorState) => void, private readonly onFullyClosed: () => void ) {} getCurrentState(): DoorState { return this.state; } // Event handlers handleOpenCommand(): void { if (this.state === DoorState.CLOSED) { this.transitionTo(DoorState.OPENING); // Simulate door mechanism (in real system, this is hardware) setTimeout(() => this.handleOpenComplete(), 1500); } } private handleOpenComplete(): void { if (this.state === DoorState.OPENING) { this.transitionTo(DoorState.OPEN); this.startDwellTimer(); } } handleCloseButtonPressed(): void { if (this.state === DoorState.OPEN) { this.cancelDwellTimer(); this.startClosing(); } } handleOpenButtonPressed(): void { if (this.state === DoorState.OPEN) { this.resetDwellTimer(); } else if (this.state === DoorState.CLOSING) { this.transitionTo(DoorState.OPENING); setTimeout(() => this.handleOpenComplete(), 1500); } } handleObstructionDetected(): void { if (this.state === DoorState.CLOSING) { // Safety: reopen doors this.transitionTo(DoorState.OPENING); setTimeout(() => this.handleOpenComplete(), 1500); } } private startClosing(): void { this.transitionTo(DoorState.CLOSING); setTimeout(() => this.handleCloseComplete(), 1500); } private handleCloseComplete(): void { if (this.state === DoorState.CLOSING) { this.transitionTo(DoorState.CLOSED); this.onFullyClosed(); } } private startDwellTimer(): void { this.dwellTimer = setTimeout(() => { if (this.state === DoorState.OPEN) { this.startClosing(); } }, this.dwellDuration); } private resetDwellTimer(): void { this.cancelDwellTimer(); this.startDwellTimer(); } private cancelDwellTimer(): void { if (this.dwellTimer) { clearTimeout(this.dwellTimer); this.dwellTimer = null; } } private transitionTo(newState: DoorState): void { console.log(`Door: ${this.state} → ${newState}`); this.state = newState; this.onStateChange(newState); } isClosed(): boolean { return this.state === DoorState.CLOSED; }}In production elevator software, the safety invariants are implemented with hardware interlocks, not just software guards. The door-movement coordination has redundant safety systems. Our software model shows the logical design; certified elevator systems add multiple physical layers.
While we could implement state machines with switch statements and conditionals, the State Pattern provides a cleaner, more extensible approach. It encapsulates state-specific behavior in dedicated classes.
State Pattern Structure:
Benefits of State Pattern:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
// State interface - defines behavior for all elevator statesinterface IElevatorState { // State identity getName(): string; // Event handlers onRequestReceived(context: ElevatorContext, request: Request): void; onFloorReached(context: ElevatorContext, floor: Floor): void; onDoorsOpened(context: ElevatorContext): void; onDoorsClosed(context: ElevatorContext): void; onEmergency(context: ElevatorContext): void; // State queries canMove(): boolean; canOpenDoors(): boolean;} // Context - the Elevator that uses statesclass ElevatorContext { private state: IElevatorState; private currentFloor: Floor; private direction: Direction = Direction.IDLE; private destinationQueue: Request[] = []; private doorStateMachine: DoorStateMachine; constructor(initialFloor: Floor) { this.currentFloor = initialFloor; this.state = new IdleState(); this.doorStateMachine = new DoorStateMachine( (newState) => this.onDoorStateChange(newState), () => this.onDoorsFullyClosed() ); } // State transition setState(newState: IElevatorState): void { console.log(`Elevator: ${this.state.getName()} → ${newState.getName()}`); this.state = newState; } // Forward events to current state handleRequest(request: Request): void { this.state.onRequestReceived(this, request); } handleFloorArrival(floor: Floor): void { this.currentFloor = floor; this.state.onFloorReached(this, floor); } // Motor control (abstracted hardware) startMotor(direction: Direction): void { this.direction = direction; console.log(`Motor started: ${direction}`); } stopMotor(): void { console.log('Motor stopped'); // Motor physically stops } // Door delegation openDoors(): void { this.doorStateMachine.handleOpenCommand(); } private onDoorStateChange(doorState: DoorState): void { if (doorState === DoorState.OPEN) { this.state.onDoorsOpened(this); } } private onDoorsFullyClosed(): void { this.state.onDoorsClosed(this); } // Queue management addDestination(request: Request): void { this.destinationQueue.push(request); this.sortDestinations(); } getNextDestination(): Request | null { return this.destinationQueue[0] ?? null; } removeCurrentDestination(): void { this.destinationQueue.shift(); } hasDestinations(): boolean { return this.destinationQueue.length > 0; } shouldStopAtFloor(floor: Floor): boolean { return this.destinationQueue.some( r => r.floor.equals(floor) ); } private sortDestinations(): void { // Sort by current direction using SCAN algorithm // Details in scheduling section } // Queries getCurrentFloor(): Floor { return this.currentFloor; } getDirection(): Direction { return this.direction; } doorsAreClosed(): boolean { return this.doorStateMachine.isClosed(); }}Concrete State Implementations:
Now let's implement each concrete state:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
// IDLE State - elevator stationary, awaiting requestsclass IdleState implements IElevatorState { getName(): string { return 'IDLE'; } onRequestReceived(context: ElevatorContext, request: Request): void { context.addDestination(request); const current = context.getCurrentFloor().getFloorNumber(); const target = request.floor.getFloorNumber(); if (target > current) { context.startMotor(Direction.UP); context.setState(new MovingUpState()); } else if (target < current) { context.startMotor(Direction.DOWN); context.setState(new MovingDownState()); } else { // Already at requested floor context.openDoors(); context.setState(new StoppedAtFloorState()); } } onFloorReached(context: ElevatorContext, floor: Floor): void { // Shouldn't happen in IDLE state } onDoorsOpened(context: ElevatorContext): void { // Doors might open in IDLE for various reasons } onDoorsClosed(context: ElevatorContext): void { // Remain in IDLE } onEmergency(context: ElevatorContext): void { context.setState(new MaintenanceState()); } canMove(): boolean { return true; } canOpenDoors(): boolean { return true; }} // MOVING_UP State - elevator traveling upwardclass MovingUpState implements IElevatorState { getName(): string { return 'MOVING_UP'; } onRequestReceived(context: ElevatorContext, request: Request): void { // Add to queue, will be handled when we reach it context.addDestination(request); } onFloorReached(context: ElevatorContext, floor: Floor): void { if (context.shouldStopAtFloor(floor)) { context.stopMotor(); context.openDoors(); context.setState(new StoppedAtFloorState()); } // Otherwise, continue moving (state unchanged) } onDoorsOpened(context: ElevatorContext): void { // Invalid - doors shouldn't open while moving throw new Error('Safety violation: doors opened while moving'); } onDoorsClosed(context: ElevatorContext): void { // Doors should already be closed while moving } onEmergency(context: ElevatorContext): void { context.stopMotor(); context.setState(new MaintenanceState()); } canMove(): boolean { return true; } canOpenDoors(): boolean { return false; }} // MOVING_DOWN State - elevator traveling downwardclass MovingDownState implements IElevatorState { getName(): string { return 'MOVING_DOWN'; } onRequestReceived(context: ElevatorContext, request: Request): void { context.addDestination(request); } onFloorReached(context: ElevatorContext, floor: Floor): void { if (context.shouldStopAtFloor(floor)) { context.stopMotor(); context.openDoors(); context.setState(new StoppedAtFloorState()); } } onDoorsOpened(context: ElevatorContext): void { throw new Error('Safety violation: doors opened while moving'); } onDoorsClosed(context: ElevatorContext): void {} onEmergency(context: ElevatorContext): void { context.stopMotor(); context.setState(new MaintenanceState()); } canMove(): boolean { return true; } canOpenDoors(): boolean { return false; }} // STOPPED_AT_FLOOR State - elevator at a floor with doors operatingclass StoppedAtFloorState implements IElevatorState { getName(): string { return 'STOPPED_AT_FLOOR'; } onRequestReceived(context: ElevatorContext, request: Request): void { context.addDestination(request); } onFloorReached(context: ElevatorContext, floor: Floor): void { // We're stopped, shouldn't receive this event } onDoorsOpened(context: ElevatorContext): void { // Now passengers can enter/exit // Remove this floor from destination queue context.removeCurrentDestination(); } onDoorsClosed(context: ElevatorContext): void { // Doors closed, decide next action if (!context.hasDestinations()) { context.setState(new IdleState()); return; } const current = context.getCurrentFloor().getFloorNumber(); const next = context.getNextDestination()!.floor.getFloorNumber(); if (next > current) { context.startMotor(Direction.UP); context.setState(new MovingUpState()); } else if (next < current) { context.startMotor(Direction.DOWN); context.setState(new MovingDownState()); } else { // Next destination is current floor (edge case) context.openDoors(); } } onEmergency(context: ElevatorContext): void { context.setState(new MaintenanceState()); } canMove(): boolean { return false; } canOpenDoors(): boolean { return true; }} // MAINTENANCE State - elevator out of serviceclass MaintenanceState implements IElevatorState { getName(): string { return 'MAINTENANCE'; } onRequestReceived(context: ElevatorContext, request: Request): void { // Ignore requests in maintenance mode console.log('Request ignored: elevator in maintenance'); } onFloorReached(context: ElevatorContext, floor: Floor): void {} onDoorsOpened(context: ElevatorContext): void {} onDoorsClosed(context: ElevatorContext): void {} onEmergency(context: ElevatorContext): void { // Already in maintenance/emergency state } // Administrative action to exit maintenance exitMaintenance(context: ElevatorContext): void { context.setState(new IdleState()); } canMove(): boolean { return false; } canOpenDoors(): boolean { return true; }}Robust state machines enforce invariants—conditions that must always be true—and validate transitions before they occur.
Key Invariants:
Transition Validation Implementation:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
// Transition validator - ensures all invariants holdclass TransitionValidator { // Valid transitions defined as (fromState, event) → toState private readonly validTransitions: Map<string, Set<string>> = new Map([ ['IDLE', new Set(['MOVING_UP', 'MOVING_DOWN', 'STOPPED_AT_FLOOR', 'MAINTENANCE'])], ['MOVING_UP', new Set(['STOPPED_AT_FLOOR', 'MOVING_UP', 'MAINTENANCE'])], ['MOVING_DOWN', new Set(['STOPPED_AT_FLOOR', 'MOVING_DOWN', 'MAINTENANCE'])], ['STOPPED_AT_FLOOR', new Set(['IDLE', 'MOVING_UP', 'MOVING_DOWN', 'MAINTENANCE'])], ['MAINTENANCE', new Set(['IDLE'])], ]); validateTransition( from: IElevatorState, to: IElevatorState, context: ElevatorContext ): ValidationResult { const fromName = from.getName(); const toName = to.getName(); // Check if transition is in valid set const validTargets = this.validTransitions.get(fromName); if (!validTargets || !validTargets.has(toName)) { return { valid: false, error: `Invalid transition: ${fromName} → ${toName}` }; } // Check invariants based on target state if (toName === 'MOVING_UP' || toName === 'MOVING_DOWN') { if (!context.doorsAreClosed()) { return { valid: false, error: 'INV-1 violation: Cannot move with doors open' }; } } if (toName === 'STOPPED_AT_FLOOR') { // Valid, moving to stopped is always allowed } return { valid: true }; }} interface ValidationResult { valid: boolean; error?: string;} // Enhanced context with validationclass ValidatedElevatorContext extends ElevatorContext { private validator: TransitionValidator = new TransitionValidator(); setState(newState: IElevatorState): void { const result = this.validator.validateTransition( this.getState(), newState, this ); if (!result.valid) { console.error(`Transition rejected: ${result.error}`); // In production, might throw or log to monitoring return; } super.setState(newState); }}Notice we validate transitions both in the concrete states (by throwing on invalid events) AND in the context (by checking before applying). This defense-in-depth catches bugs at multiple layers. In safety-critical systems, this redundancy is essential.
State machines are highly testable because they have well-defined inputs (events) and outputs (state transitions, actions). Here are strategies for comprehensive testing:
Test Categories:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
describe('Elevator State Machine', () => { let context: ElevatorContext; beforeEach(() => { const groundFloor = new Floor(0, 'Ground'); context = new ElevatorContext(groundFloor); }); describe('Idle State', () => { it('should transition to MOVING_UP on upward request', () => { const floor5Request = Request.createCabCall(new Floor(5)); context.handleRequest(floor5Request); expect(context.getState().getName()).toBe('MOVING_UP'); expect(context.getDirection()).toBe(Direction.UP); }); it('should transition to MOVING_DOWN on downward request', () => { // First move to floor 10 context = new ElevatorContext(new Floor(10)); const floor2Request = Request.createCabCall(new Floor(2)); context.handleRequest(floor2Request); expect(context.getState().getName()).toBe('MOVING_DOWN'); expect(context.getDirection()).toBe(Direction.DOWN); }); it('should stay IDLE for same-floor request with door cycle', () => { const groundRequest = Request.createCabCall(new Floor(0)); context.handleRequest(groundRequest); expect(context.getState().getName()).toBe('STOPPED_AT_FLOOR'); // Doors will open, then close, returning to IDLE }); }); describe('Moving State', () => { it('should stop at destination floor', () => { const floor5Request = Request.createCabCall(new Floor(5)); context.handleRequest(floor5Request); // Simulate arriving at floor 5 context.handleFloorArrival(new Floor(5)); expect(context.getState().getName()).toBe('STOPPED_AT_FLOOR'); }); it('should not stop at non-destination floors', () => { const floor5Request = Request.createCabCall(new Floor(5)); context.handleRequest(floor5Request); // Pass through floors 1-4 for (let i = 1; i < 5; i++) { context.handleFloorArrival(new Floor(i)); expect(context.getState().getName()).toBe('MOVING_UP'); } }); it('should add requests to queue while moving', () => { context.handleRequest(Request.createCabCall(new Floor(10))); expect(context.getState().getName()).toBe('MOVING_UP'); // Add another request while moving context.handleRequest(Request.createCabCall(new Floor(5))); // Should still be moving, but floor 5 is now in queue expect(context.hasDestinations()).toBe(true); }); }); describe('Safety Invariants', () => { it('should never move with doors open', () => { context.handleRequest(Request.createCabCall(new Floor(5))); context.handleFloorArrival(new Floor(5)); // Now in STOPPED_AT_FLOOR with doors opening // Attempt to receive a new request and move context.handleRequest(Request.createCabCall(new Floor(10))); // Should remain stopped until doors close expect(context.getState().getName()).toBe('STOPPED_AT_FLOOR'); }); it('should enter maintenance from any state', () => { // From IDLE context.handleEmergency(); expect(context.getState().getName()).toBe('MAINTENANCE'); // Reset and test from MOVING context = new ElevatorContext(new Floor(0)); context.handleRequest(Request.createCabCall(new Floor(10))); context.handleEmergency(); expect(context.getState().getName()).toBe('MAINTENANCE'); }); }); describe('Complete Scenarios', () => { it('should handle full passenger journey', () => { // Passenger at floor 3 wants floor 15 context.handleRequest(Request.createHallCall(new Floor(3), Direction.UP)); expect(context.getState().getName()).toBe('MOVING_UP'); // Arrive at floor 3 context.handleFloorArrival(new Floor(3)); expect(context.getState().getName()).toBe('STOPPED_AT_FLOOR'); // Passenger enters, presses 15 context.handleRequest(Request.createCabCall(new Floor(15))); // Doors close (simulated callback) context.simulateDoorsClosed(); expect(context.getState().getName()).toBe('MOVING_UP'); // Arrive at floor 15 context.handleFloorArrival(new Floor(15)); expect(context.getState().getName()).toBe('STOPPED_AT_FLOOR'); // Doors close, no more requests context.simulateDoorsClosed(); expect(context.getState().getName()).toBe('IDLE'); }); });});For state machines, aim for 100% transition coverage: every defined transition should be exercised by at least one test. Additionally, test that invalid transitions are rejected or handled gracefully.
State Management Mastered:
We've designed comprehensive state machines that govern elevator behavior:
What's Next:
With static entities (previous page) and dynamic behavior (this page) established, we now turn to optimization. The next page covers:
The scheduling algorithm is where elevator system design becomes truly interesting—it's the difference between a system that merely works and one that delights users with minimal wait times.
You now understand how to model complex temporal behavior using finite state machines and the State Pattern. This foundation applies far beyond elevators—any system with distinct modes of operation (orders, workflows, connection states) benefits from explicit state modeling.