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

EntityResponsibility
DirectionEnum for UP and DOWN.
ElevatorStateEnum for MOVING_UP, MOVING_DOWN, and IDLE.
DoorOpens and closes at each floor stop.
RequestHolds source floor and direction for an external request.
ElevatorOwns upQueue, downQueue, door, and floor movement logic.
ElevatorControllerScores and assigns elevators, drives per-step movement.
BuildingFacade 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 down

When 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

  1. Create a building with total floors, number of elevators, and capacity.
  2. User presses an external floor button and a Request is created.
  3. ElevatorController scores all elevators and assigns the best one.
  4. The assigned elevator adds the pickup floor to its queue.
  5. User presses a floor button inside the elevator.
  6. That floor is added directly to the elevator's queue.
  7. simulate(n) runs time steps, each elevator moves one floor per step.
  8. 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.
  • Building as a facade means Main never 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 DispatchStrategy interface so scoring can be swapped without changing ElevatorController.
  • Track currentPassengers with board/exit events instead of leaving it unused.
  • Add floor validation to reject requests outside the building range.
  • Add a MaintenanceState so individual elevators can be taken out of service.
  • Add unit tests for edge cases: all elevators full, request at current floor, empty queues.