Skip to main content
Elevator System Design
Difficulty: 🟡 Intermediate | Time: 45 minutes | Patterns: Strategy, State, Observer

🎯 Problem Statement

Design an elevator system for a building that can:
  • Handle multiple elevators
  • Optimize elevator assignment for efficiency
  • Support different scheduling algorithms
  • Handle concurrent requests from multiple floors
Why This Problem? Elevator design tests your ability to handle state machines, scheduling algorithms, and concurrency. It’s a favorite at companies like Uber and Amazon.

📋 Step 1: Clarify Requirements

Interview Tip: Elevator problems can range from simple to extremely complex. Clarify the scope!

Questions to Ask the Interviewer

CategoryQuestionImpact on Design
ScaleHow many elevators and floors?Data structures, concurrency
AlgorithmWhich scheduling algorithm?Strategy pattern selection
FeaturesVIP/express elevators?Additional elevator types
CapacityWeight limits? Max passengers?Capacity checking logic
EmergencyFire mode? Emergency stop?State machine complexity
UIDisplay current floor in elevator?Observer pattern for updates

Functional Requirements

  • Support N elevators and M floors
  • Handle up/down requests from floors (hall buttons)
  • Handle floor selection from inside elevator (car buttons)
  • Display current floor and direction
  • Handle door open/close operations
  • Emergency stop functionality

Non-Functional Requirements

  • Minimize average wait time
  • Minimize travel time (optimize scheduling)
  • Handle concurrent requests thread-safely
  • Graceful degradation if elevator fails

🧩 Step 2: Identify Core Objects

Key Insight: The elevator controller uses Strategy Pattern for scheduling algorithms - you can swap between FCFS, SCAN, and LOOK algorithms without changing the core logic.

Hardware

Elevator, Door, Floor, Button

Control

ElevatorController, Scheduler

Requests

FloorRequest, ElevatorRequest

Entity-Responsibility Mapping

EntityResponsibilitiesPattern
ElevatorMove, track floor, manage doorsState Pattern
DoorOpen/close with safety checksState Pattern
ElevatorControllerCoordinate all elevatorsSingleton
SchedulingStrategyAssign elevator to requestStrategy Pattern
FloorDisplayShow elevator positionsObserver Pattern

Scheduling Algorithms

AlgorithmDescriptionBest For
FCFSFirst come, first servedSimple buildings
SCANElevator goes one direction until endMedium buildings
LOOKLike SCAN but reverses at last requestMost efficient
NearestAssign closest idle elevatorLow traffic

📐 Step 3: Class Diagram

┌─────────────────────────────────────────────────────────────┐
│                    ElevatorController                       │
├─────────────────────────────────────────────────────────────┤
│ - elevators: List<Elevator>                                │
│ - scheduler: SchedulingStrategy                            │
│ - floorRequests: Queue<FloorRequest>                       │
├─────────────────────────────────────────────────────────────┤
│ + requestElevator(floor, direction): void                  │
│ + assignElevator(request): Elevator                        │
│ + getStatus(): List<ElevatorStatus>                        │
└─────────────────────────────────────────────────────────────┘

                              │ 1..*

┌─────────────────────────────────────────────────────────────┐
│                        Elevator                             │
├─────────────────────────────────────────────────────────────┤
│ - id: int                                                  │
│ - currentFloor: int                                        │
│ - direction: Direction                                     │
│ - state: ElevatorState                                     │
│ - destinationFloors: SortedSet<int>                        │
│ - door: Door                                               │
├─────────────────────────────────────────────────────────────┤
│ + addDestination(floor): void                              │
│ + move(): void                                             │
│ + openDoor(): void                                         │
│ + closeDoor(): void                                        │
└─────────────────────────────────────────────────────────────┘

                              │ 1

┌─────────────────────────────────────────────────────────────┐
│                          Door                               │
├─────────────────────────────────────────────────────────────┤
│ - state: DoorState                                         │
├─────────────────────────────────────────────────────────────┤
│ + open(): void                                             │
│ + close(): void                                            │
│ + isOpen(): bool                                           │
└─────────────────────────────────────────────────────────────┘

Step 4: Implementation

Enums and Constants

from enum import Enum
from typing import Optional, List, Set
from dataclasses import dataclass
from abc import ABC, abstractmethod
import threading
import time
from sortedcontainers import SortedList

class Direction(Enum):
    UP = 1
    DOWN = -1
    IDLE = 0

class ElevatorState(Enum):
    MOVING = 1
    STOPPED = 2
    MAINTENANCE = 3

class DoorState(Enum):
    OPEN = 1
    CLOSED = 2
    OPENING = 3
    CLOSING = 4

# Building configuration
NUM_FLOORS = 10
NUM_ELEVATORS = 3
FLOOR_HEIGHT_METERS = 3
ELEVATOR_SPEED = 1  # floors per second
DOOR_OPERATION_TIME = 2  # seconds

Request Classes

@dataclass
class FloorRequest:
    """Request from a floor button (external)"""
    floor: int
    direction: Direction
    timestamp: float
    
    def __hash__(self):
        return hash((self.floor, self.direction))

@dataclass
class ElevatorRequest:
    """Request from inside elevator (internal)"""
    destination_floor: int
    elevator_id: int
    timestamp: float

Door Class

class Door:
    def __init__(self):
        self._state = DoorState.CLOSED
        self._lock = threading.Lock()
    
    @property
    def state(self) -> DoorState:
        return self._state
    
    def open(self):
        with self._lock:
            if self._state == DoorState.CLOSED:
                self._state = DoorState.OPENING
                time.sleep(0.5)  # Simulate opening
                self._state = DoorState.OPEN
    
    def close(self):
        with self._lock:
            if self._state == DoorState.OPEN:
                self._state = DoorState.CLOSING
                time.sleep(0.5)  # Simulate closing
                self._state = DoorState.CLOSED
    
    def is_open(self) -> bool:
        return self._state == DoorState.OPEN

Elevator Class

class Elevator:
    def __init__(self, elevator_id: int, total_floors: int):
        self.id = elevator_id
        self.total_floors = total_floors
        self._current_floor = 1
        self._direction = Direction.IDLE
        self._state = ElevatorState.STOPPED
        self.door = Door()
        
        # Two sorted lists for efficient destination management
        self._up_stops: SortedList = SortedList()
        self._down_stops: SortedList = SortedList()
        
        self._lock = threading.Lock()
    
    @property
    def current_floor(self) -> int:
        return self._current_floor
    
    @property
    def direction(self) -> Direction:
        return self._direction
    
    @property
    def state(self) -> ElevatorState:
        return self._state
    
    def add_destination(self, floor: int):
        """Add a floor to visit"""
        with self._lock:
            if floor == self._current_floor:
                return
            
            if floor > self._current_floor:
                if floor not in self._up_stops:
                    self._up_stops.add(floor)
            else:
                if floor not in self._down_stops:
                    self._down_stops.add(floor)
            
            # Update direction if idle
            if self._direction == Direction.IDLE:
                self._update_direction()
    
    def _update_direction(self):
        """Determine next direction based on pending stops"""
        if self._up_stops:
            self._direction = Direction.UP
        elif self._down_stops:
            self._direction = Direction.DOWN
        else:
            self._direction = Direction.IDLE
    
    def get_next_stop(self) -> Optional[int]:
        """Get the next floor to stop at"""
        if self._direction == Direction.UP and self._up_stops:
            return self._up_stops[0]  # Nearest up
        elif self._direction == Direction.DOWN and self._down_stops:
            return self._down_stops[-1]  # Nearest down
        return None
    
    def move(self):
        """Move elevator one step towards destination"""
        if self._state == ElevatorState.MAINTENANCE:
            return
        
        if self.door.is_open():
            return  # Can't move with open doors
        
        next_stop = self.get_next_stop()
        if next_stop is None:
            self._update_direction()
            return
        
        self._state = ElevatorState.MOVING
        
        # Move one floor
        if self._direction == Direction.UP:
            self._current_floor += 1
        elif self._direction == Direction.DOWN:
            self._current_floor -= 1
        
        print(f"Elevator {self.id} at floor {self._current_floor}")
        
        # Check if we should stop
        if self._should_stop():
            self._stop()
    
    def _should_stop(self) -> bool:
        """Check if elevator should stop at current floor"""
        if self._direction == Direction.UP:
            return self._current_floor in self._up_stops
        elif self._direction == Direction.DOWN:
            return self._current_floor in self._down_stops
        return False
    
    def _stop(self):
        """Stop at current floor"""
        self._state = ElevatorState.STOPPED
        
        # Remove from destination lists
        if self._current_floor in self._up_stops:
            self._up_stops.remove(self._current_floor)
        if self._current_floor in self._down_stops:
            self._down_stops.remove(self._current_floor)
        
        # Open door
        self.door.open()
        print(f"Elevator {self.id} stopped at floor {self._current_floor}, doors open")
        
        time.sleep(DOOR_OPERATION_TIME)
        
        self.door.close()
        print(f"Elevator {self.id} doors closed")
        
        # Update direction
        self._update_direction()
    
    def get_status(self) -> dict:
        return {
            "id": self.id,
            "current_floor": self._current_floor,
            "direction": self._direction.name,
            "state": self._state.name,
            "pending_stops": len(self._up_stops) + len(self._down_stops)
        }
    
    def distance_to(self, floor: int) -> int:
        """Calculate distance to a floor considering direction"""
        return abs(floor - self._current_floor)
    
    def can_serve(self, floor: int, direction: Direction) -> bool:
        """Check if elevator can efficiently serve this request"""
        if self._state == ElevatorState.MAINTENANCE:
            return False
        
        if self._direction == Direction.IDLE:
            return True
        
        # Going up and floor is above, or going down and floor is below
        if self._direction == Direction.UP:
            return floor >= self._current_floor and direction == Direction.UP
        else:
            return floor <= self._current_floor and direction == Direction.DOWN

Scheduling Strategies

class SchedulingStrategy(ABC):
    """Strategy pattern for elevator scheduling"""
    
    @abstractmethod
    def select_elevator(self, elevators: List[Elevator], 
                       request: FloorRequest) -> Optional[Elevator]:
        pass

class FCFSScheduler(SchedulingStrategy):
    """First Come First Serve - assign to first available"""
    
    def select_elevator(self, elevators: List[Elevator], 
                       request: FloorRequest) -> Optional[Elevator]:
        for elevator in elevators:
            if elevator.state != ElevatorState.MAINTENANCE:
                return elevator
        return None

class ShortestSeekScheduler(SchedulingStrategy):
    """Assign to nearest elevator"""
    
    def select_elevator(self, elevators: List[Elevator], 
                       request: FloorRequest) -> Optional[Elevator]:
        best_elevator = None
        min_distance = float('inf')
        
        for elevator in elevators:
            if elevator.state == ElevatorState.MAINTENANCE:
                continue
            
            distance = elevator.distance_to(request.floor)
            if distance < min_distance:
                min_distance = distance
                best_elevator = elevator
        
        return best_elevator

class SCANScheduler(SchedulingStrategy):
    """
    SCAN (Elevator) Algorithm:
    Prefers elevators already moving in the right direction
    """
    
    def select_elevator(self, elevators: List[Elevator], 
                       request: FloorRequest) -> Optional[Elevator]:
        best_elevator = None
        best_score = float('inf')
        
        for elevator in elevators:
            if elevator.state == ElevatorState.MAINTENANCE:
                continue
            
            score = self._calculate_score(elevator, request)
            if score < best_score:
                best_score = score
                best_elevator = elevator
        
        return best_elevator
    
    def _calculate_score(self, elevator: Elevator, request: FloorRequest) -> float:
        """Lower score = better fit"""
        distance = elevator.distance_to(request.floor)
        
        # Prefer idle elevators nearby
        if elevator.direction == Direction.IDLE:
            return distance
        
        # Prefer elevators moving towards the floor in the right direction
        if elevator.can_serve(request.floor, request.direction):
            return distance * 0.5  # 50% bonus
        
        # Penalty for elevators moving away
        return distance * 2

Elevator Controller

class ElevatorController:
    """Central controller for all elevators"""
    
    def __init__(self, num_elevators: int, num_floors: int, 
                 scheduler: SchedulingStrategy = None):
        self.num_floors = num_floors
        self.elevators = [
            Elevator(i, num_floors) for i in range(num_elevators)
        ]
        self.scheduler = scheduler or SCANScheduler()
        self._pending_requests: List[FloorRequest] = []
        self._lock = threading.Lock()
        self._running = False
    
    def request_elevator(self, floor: int, direction: Direction):
        """Called when someone presses up/down button on a floor"""
        if floor < 1 or floor > self.num_floors:
            raise ValueError(f"Invalid floor: {floor}")
        
        request = FloorRequest(
            floor=floor,
            direction=direction,
            timestamp=time.time()
        )
        
        with self._lock:
            # Select best elevator
            elevator = self.scheduler.select_elevator(self.elevators, request)
            
            if elevator:
                elevator.add_destination(floor)
                print(f"Assigned elevator {elevator.id} for floor {floor} ({direction.name})")
            else:
                # All elevators busy/maintenance - queue the request
                self._pending_requests.append(request)
                print(f"All elevators busy, request queued")
    
    def select_floor(self, elevator_id: int, floor: int):
        """Called when someone presses a floor button inside elevator"""
        if floor < 1 or floor > self.num_floors:
            raise ValueError(f"Invalid floor: {floor}")
        
        if elevator_id < 0 or elevator_id >= len(self.elevators):
            raise ValueError(f"Invalid elevator: {elevator_id}")
        
        self.elevators[elevator_id].add_destination(floor)
        print(f"Elevator {elevator_id}: selected floor {floor}")
    
    def run(self):
        """Main loop - run in separate thread"""
        self._running = True
        
        while self._running:
            for elevator in self.elevators:
                elevator.move()
            
            # Process pending requests
            self._process_pending_requests()
            
            time.sleep(1)  # Simulate time between movements
    
    def _process_pending_requests(self):
        """Try to assign pending requests"""
        with self._lock:
            still_pending = []
            
            for request in self._pending_requests:
                elevator = self.scheduler.select_elevator(self.elevators, request)
                if elevator:
                    elevator.add_destination(request.floor)
                else:
                    still_pending.append(request)
            
            self._pending_requests = still_pending
    
    def stop(self):
        self._running = False
    
    def get_status(self) -> List[dict]:
        return [e.get_status() for e in self.elevators]
    
    def set_maintenance(self, elevator_id: int, maintenance: bool):
        """Put elevator in/out of maintenance mode"""
        if elevator_id < 0 or elevator_id >= len(self.elevators):
            return
        
        elevator = self.elevators[elevator_id]
        elevator._state = (ElevatorState.MAINTENANCE if maintenance 
                          else ElevatorState.STOPPED)

Display Panel

class DisplayPanel:
    """Display showing elevator status"""
    
    def __init__(self, controller: ElevatorController):
        self.controller = controller
    
    def show(self):
        """Display current status of all elevators"""
        print("\n" + "=" * 50)
        print("ELEVATOR STATUS")
        print("=" * 50)
        
        for status in self.controller.get_status():
            direction_arrow = {
                "UP": "↑",
                "DOWN": "↓",
                "IDLE": "○"
            }[status["direction"]]
            
            print(f"Elevator {status['id']}: "
                  f"Floor {status['current_floor']} "
                  f"{direction_arrow} "
                  f"[{status['state']}] "
                  f"({status['pending_stops']} stops)")
        
        print("=" * 50)

Step 5: Usage Example

import threading

# Create controller with 3 elevators and 10 floors
controller = ElevatorController(
    num_elevators=3,
    num_floors=10,
    scheduler=SCANScheduler()
)

# Create display
display = DisplayPanel(controller)

# Start elevator system in background
controller_thread = threading.Thread(target=controller.run)
controller_thread.start()

# Simulate requests
controller.request_elevator(floor=5, direction=Direction.UP)
controller.request_elevator(floor=3, direction=Direction.DOWN)
controller.select_floor(elevator_id=0, floor=8)

time.sleep(2)
display.show()

# Person gets in on floor 5, wants to go to floor 9
controller.select_floor(elevator_id=0, floor=9)

time.sleep(5)
display.show()

# Stop the system
controller.stop()
controller_thread.join()

Scheduling Algorithm Comparison

AlgorithmProsConsBest For
FCFSSimple, fairCan be inefficientLow traffic
Shortest SeekMinimizes wait for nearestStarvation possibleMedium traffic
SCANNo starvation, efficientLonger max waitHigh traffic
LOOKMore efficient than SCANComplexHigh-rise buildings

Key Design Decisions

Different buildings have different needs. A hospital may want FCFS for fairness, while an office may want SCAN for efficiency. Strategy pattern allows swapping algorithms without changing core logic.
Separating up and down stops allows efficient retrieval of the next stop in each direction. A single list would require filtering by direction each time.
Multiple threads can request elevators simultaneously (buttons on different floors). Locks prevent race conditions when modifying elevator state.
Interview Extension: Be ready to discuss VIP/priority floors, weight limits, fire emergency mode, or energy-efficient algorithms that minimize empty trips.