LLD Question 03
Elevator System
Design an elevator system with a building, multiple elevators, external floor requests, internal button presses, and a score-based dispatcher.
Learning note
Keeping upQueue and downQueue inside the elevator makes SCAN-like movement natural. The controller only needs to score and assign.
Problem Statement
Design an elevator system for a building with multiple floors and multiple elevators. Users should be able to request an elevator from a floor panel and press floor buttons from inside the elevator.
Functional Requirements
- A building supports multiple elevators and multiple floors.
- A user can request an elevator from a floor by pressing UP or DOWN.
- A user can select a destination floor from inside the elevator.
- The system should dispatch the best elevator based on direction and distance.
- The elevator moves one floor per time step.
- The door opens and closes at each stop.
- The simulation runs for a configurable number of time steps.
Core Entities
| Entity | Responsibility |
|---|---|
Direction | Enum for UP and DOWN. |
ElevatorState | Enum for MOVING_UP, MOVING_DOWN, and IDLE. |
Door | Opens and closes at each floor stop. |
Request | Holds source floor and direction for an external request. |
Elevator | Owns upQueue, downQueue, door, and floor movement logic. |
ElevatorController | Scores and assigns elevators, drives per-step movement. |
Building | Facade that owns elevators and exposes the public API. |
Design Decisions
Two Queues Per Elevator
Each elevator maintains two separate priority queues:
upQueue floors to visit while moving up
downQueue floors to visit while moving downWhen a stop is added, it goes to the correct queue based on whether the target floor is above or below the current floor.
This naturally implements SCAN-like movement: finish going up, then come down.
Score-Based Dispatching
The controller scores every elevator for each request:
Same direction and request is ahead low score (best)
Elevator is idle medium score
Different direction or going past high score (worst)The elevator with the lowest score is assigned.
Building As Facade
Building does not own any elevator logic directly. It delegates to ElevatorController. This keeps Main clean and simple.
Step-Based Simulation
Elevators move one floor per step() call. Building.simulate(n) drives n steps so movement is observable without threading.
Main Flow
- Create a building with total floors, number of elevators, and capacity.
- User presses an external floor button and a
Requestis created. ElevatorControllerscores all elevators and assigns the best one.- The assigned elevator adds the pickup floor to its queue.
- User presses a floor button inside the elevator.
- That floor is added directly to the elevator's queue.
simulate(n)runs time steps, each elevator moves one floor per step.- At each target floor the elevator opens and closes the door.
Complete Code: Bottom-Up
Read this section starting from the smallest enums first, then move up through request objects, elevator behavior, the controller, and finally Building, where all classes converge.
1. Direction.java
public enum Direction {
UP, DOWN
}2. ElevatorState.java
public enum ElevatorState {
MOVING_UP, MOVING_DOWN, IDLE
}3. Door.java
public class Door {
private boolean isOpen;
public void open(){
isOpen = true;
System.out.println("Door is Opened");
}
public void close(){
isOpen = false;
System.out.println("Door is Closed");
}
public boolean isOpen(){
return isOpen;
}
}4. Request.java
public class Request {
private final int sourceFloor;
private final Direction direction;
public Request(int sourceFloor, Direction direction){
this.sourceFloor = sourceFloor;
this.direction = direction;
}
public int getSourceFloor() {
return sourceFloor;
}
public Direction getDirection() {
return direction;
}
@Override
public String toString() {
return "Request{floor=" + sourceFloor + ", dir=" + direction + "}";
}
}5. Elevator.java
import java.util.PriorityQueue;
public class Elevator {
private final int id;
private final Door door;
private int currentFloor;
private ElevatorState elevatorState;
private final PriorityQueue<Integer> upQueue;
private final PriorityQueue<Integer> downQueue;
private final int capacity;
private int currentPassengers;
public Elevator(int id, int capacity){
this.id = id;
this.door = new Door();
this.currentFloor = 0;
this.elevatorState = ElevatorState.IDLE;
this.upQueue = new PriorityQueue<>();
this.downQueue = new PriorityQueue<>();
this.capacity = capacity;
this.currentPassengers = 0;
}
public void addStop(int floorNumber){
if(floorNumber > currentFloor){
upQueue.add(floorNumber);
}
else if(floorNumber < currentFloor){
downQueue.add(floorNumber);
}
else
openAndClose();
if(elevatorState == ElevatorState.IDLE)
determineDirection();
}
private void determineDirection(){
if(elevatorState == ElevatorState.MOVING_UP || elevatorState == ElevatorState.IDLE){
if(!upQueue.isEmpty())
elevatorState = ElevatorState.MOVING_UP;
else if(!downQueue.isEmpty())
elevatorState = ElevatorState.MOVING_DOWN;
else{
elevatorState = ElevatorState.IDLE;
System.out.println(" [Elevator " + id + "] Now IDLE at floor " + currentFloor);
}
}
else{
if(!downQueue.isEmpty())
elevatorState = ElevatorState.MOVING_DOWN;
else if(!upQueue.isEmpty())
elevatorState = ElevatorState.MOVING_UP;
else {
elevatorState = ElevatorState.IDLE;
System.out.println(" [Elevator " + id + "] Now IDLE at floor " + currentFloor);
}
}
}
public void pressFloorButton(int floorNumber){
addStop(floorNumber);
}
public void openAndClose(){
System.out.println(" [Elevator " + id + "] Stopped at floor " + currentFloor);
door.open();
door.close();
}
public void step(){
if(ElevatorState.MOVING_UP == elevatorState && !upQueue.isEmpty()){
currentFloor++;
System.out.println(" [Elevator " + id + "] Moving UP -> Floor " + currentFloor);
if(upQueue.peek() == currentFloor){
upQueue.poll();
openAndClose();
}
}
if(ElevatorState.MOVING_DOWN == elevatorState && !downQueue.isEmpty()){
currentFloor--;
System.out.println(" [Elevator " + id + "] Moving DOWN -> Floor " + currentFloor);
if(downQueue.peek() == currentFloor){
downQueue.poll();
openAndClose();
}
}
determineDirection();
}
public ElevatorState getElevatorState() {
return elevatorState;
}
public int getCurrentFloor() {
return currentFloor;
}
public boolean isFull(){
return currentPassengers >= capacity;
}
public boolean isIdle(){
return elevatorState == ElevatorState.IDLE;
}
public int getId() {
return id;
}
}6. ElevatorController.java
import java.util.List;
public class ElevatorController {
List<Elevator> elevators;
public ElevatorController(List<Elevator> elevators){
this.elevators = elevators;
}
public void handleRequest(Request request){
System.out.println("\nIncoming " + request);
Elevator best = findBestElevator(request);
System.out.println(" Assigned to Elevator " + best.getId());
best.addStop(request.getSourceFloor());
}
public void handleInternalRequest(int elevatorId, int floorNum){
elevators.stream()
.filter(e -> e.getId() == elevatorId)
.findFirst()
.ifPresent(e -> e.pressFloorButton(floorNum));
}
private Elevator findBestElevator(Request request){
Elevator best = null;
int bestScore = Integer.MAX_VALUE;
for(Elevator elevator: elevators){
if(elevator.isFull()) continue;
int score = calculateScore(request, elevator);
if(score < bestScore){
best = elevator;
bestScore = score;
}
}
return best;
}
private int calculateScore(Request request, Elevator elevator){
int distance = Math.abs(elevator.getCurrentFloor() - request.getSourceFloor());
if(elevator.getElevatorState() == ElevatorState.MOVING_UP
&& request.getDirection() == Direction.UP
&& request.getSourceFloor() > elevator.getCurrentFloor())
return distance;
if(elevator.getElevatorState() == ElevatorState.MOVING_DOWN
&& request.getDirection() == Direction.DOWN
&& request.getSourceFloor() < elevator.getCurrentFloor())
return distance;
if(elevator.isIdle())
return 100 + distance;
return 1000 + distance;
}
public void step(){
for(Elevator elevator: elevators){
elevator.step();
}
}
}7. Building.java
import java.util.ArrayList;
import java.util.List;
public class Building {
private final List<Elevator> elevators;
private final ElevatorController elevatorController;
private final int totalFloors;
public Building(int totalFloors, int numOfElevators, int eleCap){
this.totalFloors = totalFloors;
this.elevators = new ArrayList<>();
for(int i=0;i<numOfElevators;i++){
elevators.add(new Elevator(i, eleCap));
}
this.elevatorController = new ElevatorController(elevators);
System.out.println("Building initialized: " + totalFloors + " floors, "
+ numOfElevators + " elevators\n");
}
public void handleRequest(int sourceFloor, Direction direction){
elevatorController.handleRequest(new Request(sourceFloor, direction));
}
public void pressFloorButton(int targetFloor, int elevatorId){
elevatorController.handleInternalRequest(elevatorId, targetFloor);
}
public void simulate(int cnt){
System.out.println("\nSimulating " + cnt + " time steps...");
for(int i=0;i<cnt;i++){
System.out.println("\n-- Step " + i + " --");
elevatorController.step();
}
}
}8. Main.java
public class Main {
public static void main(String[] args) {
Building building = new Building(10, 3, 8);
building.handleRequest(5, Direction.UP); // Someone on floor 5 wants to go up
building.handleRequest(3, Direction.DOWN); // Someone on floor 3 wants to go down
building.handleRequest(8, Direction.UP); // Someone on floor 8 wants to go up
// Passenger inside Elevator 1 selects floor 7
// Note: pressFloorButton(targetFloor, elevatorId)
building.pressFloorButton(7, 1);
// Simulate 10 time steps (each step = elevator moves 1 floor)
building.simulate(10);
}
}What I Learned
- Two queues per elevator naturally encode SCAN movement without extra logic.
- Score-based dispatching is flexible because the scoring function is the only thing that changes when rules change.
Buildingas a facade meansMainnever touches controllers or elevators directly.- Step-based simulation is a clean way to observe movement in an LLD demo without threading.
Possible Improvements
- Add a
DispatchStrategyinterface so scoring can be swapped without changingElevatorController. - Track
currentPassengerswith board/exit events instead of leaving it unused. - Add floor validation to reject requests outside the building range.
- Add a
MaintenanceStateso individual elevators can be taken out of service. - Add unit tests for edge cases: all elevators full, request at current floor, empty queues.