An elevator system is a combination of mechanical components and software logic used in multi-story buildings to transport people or goods vertically between floors.
Modern elevator systems typically consist of one or more elevator cars (also called lifts), each controlled by an embedded software system that manages a range of operations, including:
In this chapter, we will explore the low-level design of an elevator system in detail.
Lets start by clarifying the requirements:
Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions and better define the scope of the system.
Here is an example of how a conversation between the candidate and the interviewer might unfold:
Candidate: Should we assume a single elevator or support multiple elevators operating in the same building?
Interviewer: Let’s design for a system with multiple elevators in the same building.
Candidate: Should the system handle both internal requests (floor buttons inside the elevator) and external requests (hall calls from each floor)?
Interviewer: Yes, the system should support both internal and external requests.
Candidate: Should we follow a specific elevator scheduling algorithm, like SCAN or LOOK, or is a basic first-come-first-serve strategy sufficient?
Interviewer: For this version, use a simple scheduling strategy like nearest elevator first. However, the design should be extensible enough to support pluggable scheduling algorithms in the future.
Candidate: Should the elevators display their current floor and movement direction (up/down)?
Interviewer: Yes, each elevator should display its current floor and movement direction in real-time.
After gathering the details, we can summarize the key system requirements.
After the requirements are clear, lets identify the core entities/objects we will have in our system.
Core entities are the fundamental building blocks of our system. We identify them by analyzing key nouns (e.g., elevator, request, floor, direction, display) and actions (e.g., move, assign, display, handle) from the functional requirements. These typically translate into classes, enums, or interfaces in an object-oriented design.
Below, we break down the functional requirements and extract the relevant entities. Related requirements are grouped together when they correspond to the same conceptual entities.
This clearly suggests the need for an Elevator entity that encapsulates the state and behavior of an individual elevator.
We also need an ElevatorSystem (or ElevatorController) entity to manage all elevators, process incoming requests, and delegate tasks to the appropriate elevator.
This implies a Request entity that captures floor requests, their origin (inside or outside the elevator), and direction.
To represent direction, we define a Direction enum with values UP and DOWN.
This leads to the need for an ElevatorDisplay entity. Each elevator is equipped with a display component that continuously updates and shows the elevator’s current floor and movement direction.
ElevatorSystem: Manages all elevators in the building and routes external requests to the best-suited elevator.Elevator: Represents a single elevator with attributes like current floor, direction, request queue, and operational state.Request: Encapsulates a user’s request to go to or from a floor, with associated direction and type.Direction (Enum): Represents the intended direction of travel—UP or DOWN.ElevatorDisplay: Shows current floor and movement direction of an elevator in real time.Direction: A simple enumeration (UP, DOWN, IDLE) that defines the possible movement states of an elevator. This is crucial for both request handling and state management.RequestSource: Distinguishes between requests made from inside the elevator cabin (INTERNAL) and those from a building floor (EXTERNAL). This helps in prioritizing and routing requests correctly.RequestA data-centric class that encapsulates all information about a user's request.
It holds the targetFloor, the desired direction (for external requests), and the source. This makes passing request information throughout the system clean and simple.
Elevator This is the central active object representing a single elevator car.
It implements Runnable to operate on its own thread, manages its internal request queues (upRequests, downRequests), and maintains its current state (IdleState, MovingUpState, etc.). It also acts as the "Subject" in the Observer pattern.
ElevatorSystemThe main controller and entry point for the entire system.
It's a Singleton that initializes and manages all Elevator instances. It acts as a Facade, providing a simple API for external and internal requests while hiding the underlying complexity of elevator selection and request dispatching.
The relationships between classes are designed to promote low coupling and high cohesion.
A "has-a" relationship where the part cannot exist without the whole
ElevatorSystem has an ElevatorSelectionStrategy: The strategy is an essential, integral part of the system, created and owned by it.ElevatorSystem has Elevators: The system creates, manages, and contains the elevators. The elevators' lifecycle is directly controlled by the ElevatorSystem and its thread pool.Elevator has an ElevatorState: Every elevator has a state object that defines its current behavior. The elevator manages the lifecycle of its state objects.A "has-a" relationship where the part can exist independently
Elevator has ElevatorObservers: The Elevator holds a list of observers (like Display), but the observers are created externally and can exist independently of the elevator.IdleState, MovingUpState, and MovingDownState implement the ElevatorState interface.NearestElevatorStrategy implements the ElevatorSelectionStrategy interface.Display implements the ElevatorObserver interface.Elevator implements the Runnable interface, making it an active object that can run on a thread.Elevator processes Requests: An elevator maintains collections of requests it needs to serve.ElevatorSystem receives and delegates Requests to the appropriate elevator.Several design patterns are employed to create a robust and flexible system architecture.
This pattern is used to select an elevator for an external request.
ElevatorSystemElevatorSelectionStrategyNearestElevatorStrategyElevatorSystem.This pattern manages the complex, state-dependent behavior of an elevator.
ElevatorElevatorStateIdleState, MovingUpState, MovingDownStateElevator class becomes simpler, as it just delegates actions to the current state object. This makes the code easier to understand and extend (e.g., adding a MaintenanceState).This pattern provides a way to notify multiple objects about state changes in an elevator.
ElevatorElevatorObserver (implemented by Display)Elevator from its observers. We can attach any number of displays, loggers, or monitoring tools to an elevator without changing its code. The elevator simply notifies all registered observers when its floor or direction changes.The ElevatorSystem acts as a facade. It provides a simple, unified interface (requestElevator(), selectFloor()) to the more complex underlying subsystem of elevators, states, strategies, and threads. This simplifies the interaction for the client (ElevatorSystemDemo).
The ElevatorSystem class is implemented as a Singleton to ensure there is only one instance controlling the entire building's elevator network. This provides a single, global point of access and prevents conflicting states.
Direction and RequestSource1class Direction(Enum):
2 UP = "UP"
3 DOWN = "DOWN"
4 IDLE = "IDLE"
5
6class RequestSource(Enum):
7 INTERNAL = "INTERNAL" # From inside the cabin
8 EXTERNAL = "EXTERNAL" # From the hall/floorDirection defines the elevator’s movement state.RequestSource distinguishes between cabin (internal) and hall (external) requests—critical for prioritizing and routing.Encapsulates all the information needed to process a user's request.
1@dataclass
2class Request:
3 target_floor: int
4 direction: Direction # Primarily for External requests
5 source: RequestSource
6
7 def __str__(self):
8 if self.source == RequestSource.EXTERNAL:
9 return f"{self.source.value} Request to floor {self.target_floor} going {self.direction.value}"
10 else:
11 return f"{self.source.value} Request to floor {self.target_floor}"To provide real-time updates on the elevator's status without coupling the Elevator class to a specific display mechanism, we use the Observer pattern.
1class ElevatorObserver(ABC):
2 @abstractmethod
3 def update(self, elevator):
4 pass
5
6class Display(ElevatorObserver):
7 def update(self, elevator):
8 print(f"[DISPLAY] Elevator {elevator.get_id()} | Current Floor: {elevator.get_current_floor()} | Direction: {elevator.get_direction().value}")The Elevator is the "Subject" and Display is the "Observer". The Elevator notifies all its registered observers (notifyObservers()) whenever its state (floor, direction) changes. This allows us to add any number of different observers (e.g., a logging service, a graphical UI, a maintenance monitor) without modifying the Elevator class.
To make the algorithm for assigning an external request to an elevator pluggable, we use the Strategy pattern.
1class ElevatorSelectionStrategy(ABC):
2 @abstractmethod
3 def select_elevator(self, elevators: List, request: Request) -> Optional:
4 pass
5
6class NearestElevatorStrategy(ElevatorSelectionStrategy):
7 def select_elevator(self, elevators: List, request: Request) -> Optional:
8 best_elevator = None
9 min_distance = float('inf')
10
11 for elevator in elevators:
12 if self._is_suitable(elevator, request):
13 distance = abs(elevator.get_current_floor() - request.target_floor)
14 if distance < min_distance:
15 min_distance = distance
16 best_elevator = elevator
17
18 return best_elevator
19
20 def _is_suitable(self, elevator, request: Request) -> bool:
21 if elevator.get_direction() == Direction.IDLE:
22 return True
23 if elevator.get_direction() == request.direction:
24 if request.direction == Direction.UP and elevator.get_current_floor() <= request.target_floor:
25 return True
26 if request.direction == Direction.DOWN and elevator.get_current_floor() >= request.target_floor:
27 return True
28 return FalseElevatorSelectionStrategy interface defines a contract for any selection algorithm. By programming to this interface, the main ElevatorSystem is decoupled from the specific implementation of the selection logic. We could easily introduce new strategies (e.g., "least busy elevator," "energy-saving elevator") without changing the core system.
NearestElevatorStrategy selects the best elevator for a hall request using the nearest moving-in-same-direction or idle heuristic.
The behavior of an elevator (how it moves, how it accepts new requests) changes drastically depending on whether it's idle, moving up, or moving down. The State pattern is a perfect fit to manage this complexity.
ElevatorStateThis interface defines the operations that depend on the elevator's state. The Elevator class will delegate calls to these methods to its current state object, effectively changing its behavior by changing its state object.
1class ElevatorState(ABC):
2 @abstractmethod
3 def move(self, elevator):
4 pass
5
6 @abstractmethod
7 def add_request(self, elevator, request: Request):
8 pass
9
10 @abstractmethod
11 def get_direction(self) -> Direction:
12 passEach state class encapsulates the logic for that specific state. For example, MovingUpState only concerns itself with servicing the next highest floor in its queue.
1class IdleState(ElevatorState):
2 def move(self, elevator):
3 if elevator.get_up_requests():
4 elevator.set_state(MovingUpState())
5 elif elevator.get_down_requests():
6 elevator.set_state(MovingDownState())
7 # Else stay idle
8
9 def add_request(self, elevator, request: Request):
10 if request.target_floor > elevator.get_current_floor():
11 elevator.get_up_requests().add(request.target_floor)
12 elif request.target_floor < elevator.get_current_floor():
13 elevator.get_down_requests().add(request.target_floor)
14 # If request is for current floor, doors would open (handled implicitly by moving to that floor)
15
16 def get_direction(self) -> Direction:
17 return Direction.IDLE1class MovingUpState(ElevatorState):
2 def move(self, elevator):
3 if not elevator.get_up_requests():
4 elevator.set_state(IdleState())
5 return
6
7 next_floor = min(elevator.get_up_requests())
8 elevator.set_current_floor(elevator.get_current_floor() + 1)
9
10 if elevator.get_current_floor() == next_floor:
11 print(f"Elevator {elevator.get_id()} stopped at floor {next_floor}")
12 elevator.get_up_requests().remove(next_floor)
13
14 if not elevator.get_up_requests():
15 elevator.set_state(IdleState())
16
17 def add_request(self, elevator, request: Request):
18 # Internal requests always get added to the appropriate queue
19 if request.source == RequestSource.INTERNAL:
20 if request.target_floor > elevator.get_current_floor():
21 elevator.get_up_requests().add(request.target_floor)
22 else:
23 elevator.get_down_requests().add(request.target_floor)
24 return
25
26 # External requests
27 if request.direction == Direction.UP and request.target_floor >= elevator.get_current_floor():
28 elevator.get_up_requests().add(request.target_floor)
29 elif request.direction == Direction.DOWN:
30 elevator.get_down_requests().add(request.target_floor)
31
32 def get_direction(self) -> Direction:
33 return Direction.UPThe states are responsible for managing transitions. For instance, when MovingUpState has no more up-requests, it transitions the elevator's state to IdleState (or MovingDownState if down-requests exist). This keeps the transition logic clean and localized.
1class MovingDownState(ElevatorState):
2 def move(self, elevator):
3 if not elevator.get_down_requests():
4 elevator.set_state(IdleState())
5 return
6
7 next_floor = max(elevator.get_down_requests())
8 elevator.set_current_floor(elevator.get_current_floor() - 1)
9
10 if elevator.get_current_floor() == next_floor:
11 print(f"Elevator {elevator.get_id()} stopped at floor {next_floor}")
12 elevator.get_down_requests().remove(next_floor)
13
14 if not elevator.get_down_requests():
15 elevator.set_state(IdleState())
16
17 def add_request(self, elevator, request: Request):
18 # Internal requests always get added to the appropriate queue
19 if request.source == RequestSource.INTERNAL:
20 if request.target_floor > elevator.get_current_floor():
21 elevator.get_up_requests().add(request.target_floor)
22 else:
23 elevator.get_down_requests().add(request.target_floor)
24 return
25
26 # External requests
27 if request.direction == Direction.DOWN and request.target_floor <= elevator.get_current_floor():
28 elevator.get_down_requests().add(request.target_floor)
29 elif request.direction == Direction.UP:
30 elevator.get_up_requests().add(request.target_floor)
31
32 def get_direction(self) -> Direction:
33 return Direction.DOWNThe Elevator class brings together the State and Observer patterns. It runs in its own thread to simulate independent operation.
1class Elevator:
2 def __init__(self, elevator_id: int):
3 self.id = elevator_id
4 self.current_floor = 1
5 self.current_floor_lock = threading.Lock()
6 self.state = IdleState()
7 self.is_running = True
8
9 self.up_requests = set()
10 self.down_requests = set()
11
12 # Observer Pattern: List of observers
13 self.observers = []
14
15 # --- Observer Pattern Methods ---
16 def add_observer(self, observer: ElevatorObserver):
17 self.observers.append(observer)
18 observer.update(self) # Send initial state
19
20 def notify_observers(self):
21 for observer in self.observers:
22 observer.update(self)
23
24 # --- State Pattern Methods ---
25 def set_state(self, state: ElevatorState):
26 self.state = state
27 self.notify_observers() # Notify observers on direction change
28
29 def move(self):
30 self.state.move(self)
31
32 # --- Request Handling ---
33 def add_request(self, request: Request):
34 print(f"Elevator {self.id} processing: {request}")
35 self.state.add_request(self, request)
36
37 # --- Getters and Setters ---
38 def get_id(self) -> int:
39 return self.id
40
41 def get_current_floor(self) -> int:
42 with self.current_floor_lock:
43 return self.current_floor
44
45 def set_current_floor(self, floor: int):
46 with self.current_floor_lock:
47 self.current_floor = floor
48 self.notify_observers() # Notify observers on floor change
49
50 def get_direction(self) -> Direction:
51 return self.state.get_direction()
52
53 def get_up_requests(self) -> Set[int]:
54 return self.up_requests
55
56 def get_down_requests(self) -> Set[int]:
57 return self.down_requests
58
59 def is_elevator_running(self) -> bool:
60 return self.is_running
61
62 def stop_elevator(self):
63 self.is_running = False
64
65 def run(self):
66 while self.is_running:
67 self.move()
68 try:
69 time.sleep(1) # Simulate movement time
70 except KeyboardInterrupt:
71 self.is_running = False
72 breakExplanation:
This class acts as the central coordinator and public-facing API (Facade) for the entire system.
1class ElevatorSystem:
2 _instance = None
3 _lock = threading.Lock()
4
5 def __new__(cls, num_elevators: int):
6 if cls._instance is None:
7 with cls._lock:
8 if cls._instance is None:
9 cls._instance = super().__new__(cls)
10 cls._instance._initialized = False
11 return cls._instance
12
13 def __init__(self, num_elevators: int):
14 if self._initialized:
15 return
16
17 self.selection_strategy = NearestElevatorStrategy()
18 self.executor_service = ThreadPoolExecutor(max_workers=num_elevators)
19
20 elevator_list = []
21 display = Display() # Create the observer
22
23 for i in range(1, num_elevators + 1):
24 elevator = Elevator(i)
25 elevator.add_observer(display) # Attach the observer
26 elevator_list.append(elevator)
27
28 self.elevators = {elevator.get_id(): elevator for elevator in elevator_list}
29 self._initialized = True
30
31 @classmethod
32 def get_instance(cls, num_elevators: int):
33 return cls(num_elevators)
34
35 def start(self):
36 for elevator in self.elevators.values():
37 self.executor_service.submit(elevator.run)
38
39 # --- Facade Methods ---
40
41 # EXTERNAL Request (Hall Call)
42 def request_elevator(self, floor: int, direction: Direction):
43 print(f"\n>> EXTERNAL Request: User at floor {floor} wants to go {direction.value}")
44 request = Request(floor, direction, RequestSource.EXTERNAL)
45
46 # Use strategy to find the best elevator
47 selected_elevator = self.selection_strategy.select_elevator(list(self.elevators.values()), request)
48
49 if selected_elevator:
50 selected_elevator.add_request(request)
51 else:
52 print("System busy, please wait.")
53
54 # INTERNAL Request (Cabin Call)
55 def select_floor(self, elevator_id: int, destination_floor: int):
56 print(f"\n>> INTERNAL Request: User in Elevator {elevator_id} selected floor {destination_floor}")
57 request = Request(destination_floor, Direction.IDLE, RequestSource.INTERNAL)
58
59 elevator = self.elevators.get(elevator_id)
60 if elevator:
61 elevator.add_request(request)
62 else:
63 print("Invalid elevator ID.", file=sys.stderr)
64
65 def shutdown(self):
66 print("Shutting down elevator system...")
67 for elevator in self.elevators.values():
68 elevator.stop_elevator()
69 self.executor_service.shutdown()The class is a Singleton to ensure there's only one control system. It provides a simple, clean API (requestElevator, selectFloor) that hides the internal complexity of strategies, states, and threads.
It uses a FixedThreadPool to manage the Elevator threads. This provides a bounded resource pool for the active elevator objects.
The main method demonstrates the end-to-end flow of the system, simulating user interactions and showing how the different components work together.
1class ElevatorSystemDemo:
2 @staticmethod
3 def main():
4 import sys
5
6 # Setup: A building with 2 elevators
7 num_elevators = 2
8 # The get_instance method now initializes the elevators and attaches the Display (Observer).
9 elevator_system = ElevatorSystem.get_instance(num_elevators)
10
11 # Start the elevator system
12 elevator_system.start()
13 print("Elevator system started. ConsoleDisplay is observing.\n")
14
15 # --- SIMULATION START ---
16
17 # 1. External Request: User at floor 5 wants to go UP.
18 # The system will dispatch this to the nearest elevator (likely E1 or E2, both at floor 1).
19 elevator_system.request_elevator(5, Direction.UP)
20 time.sleep(0.1) # Wait for the elevator to start moving
21
22 # 2. Internal Request: Assume E1 took the previous request.
23 # The user gets in at floor 5 and presses 10.
24 # We send this request directly to E1.
25
26 # Note: In a real simulation, we'd wait until E1 reaches floor 5, but for this demo,
27 # we simulate the internal button press shortly after the external one.
28 elevator_system.select_floor(1, 10)
29 time.sleep(0.2)
30
31 # 3. External Request: User at floor 3 wants to go DOWN.
32 # E2 (likely still idle at floor 1) might take this, or E1 if it's convenient.
33 elevator_system.request_elevator(3, Direction.DOWN)
34 time.sleep(0.3)
35
36 # 4. Internal Request: User in E2 presses 1.
37 elevator_system.select_floor(2, 1)
38
39 # Let the simulation run for a while to observe the display updates
40 print("\n--- Letting simulation run for 1 second ---")
41 time.sleep(1)
42
43 # Shutdown the system
44 elevator_system.shutdown()
45 print("\n--- SIMULATION END ---")
46
47if __name__ == "__main__":
48 ElevatorSystemDemo.main()What is the main reason to introduce a 'Request' entity in the Elevator System design?
No comments yet. Be the first to comment!