LLD Question 03

Elevator System

Design an elevator system with buildings, multiple elevators, floor requests, internal requests, dispatching, and movement strategies.

Learning note

Elevator design becomes easier when request dispatching and elevator movement are treated as separate strategies.

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 select a destination from inside the elevator.

Functional Requirements

  • A building must support multiple elevators and multiple floors.
  • A user can request an elevator through an external panel with up/down buttons.
  • A user can select the destination floor through an internal panel.
  • The system should decide which elevator to dispatch with minimal effort.
  • The system should support movement strategies like FCFS and SCAN.
  • Users should only exit when the elevator is stopped and doors are open.
  • Elevators can be moved to maintenance mode.

Core Entities

EntityResponsibility
BuildingOwns building metadata and elevators.
ElevatorRepresents one elevator car with floor, direction, state, load, and door.
ExternalRequestRequest made from a floor panel.
InternalRequestRequest made from inside an elevator.
DispatchStrategySelects the best elevator for an external request.
MovementStrategyDecides how one elevator consumes destination requests.
ElevatorControllerControls one elevator and its pending destinations.
ElevatorSystemMain orchestrator where building, controllers, and strategies converge.

Design Decisions

Separate Dispatching From Movement

Dispatching answers: which elevator should handle this floor request?

Movement answers: in what order should this elevator visit its pending floors?

Keeping them separate avoids mixing two different decisions in one class.

Strategy for Dispatching

The first implementation uses NearestElevatorStrategy, but the system can later support capacity-aware, direction-aware, or load-balanced strategies.

Strategy for Movement

The elevator can use FCFSMovementStrategy for simple request ordering or ScanMovementStrategy to continue in one direction before reversing.

Controller Per Elevator

Each elevator has an ElevatorController. This keeps pending destination queues close to the elevator they control and avoids putting every responsibility inside ElevatorSystem.

Main Flow

  1. Create a building with min floor, max floor, and elevators.
  2. Create one controller per elevator.
  3. User presses an external floor button.
  4. ElevatorSystem asks DispatchStrategy to select an elevator.
  5. The selected elevator controller receives the pickup floor.
  6. User enters the elevator and selects a destination floor.
  7. The controller adds the destination request.
  8. Movement strategy decides the next floor to visit.
  9. Elevator moves, opens doors, closes doors, and continues.

Complete Code: Bottom-Up

Read this section from enums and request objects first, then move into elevator behavior, strategies, controllers, and finally ElevatorSystem, where all classes converge.

1. Direction.java

public enum Direction {
    UP,
    DOWN,
    IDLE
}

2. ElevatorState.java

public enum ElevatorState {
    MOVING,
    STOPPED,
    DOORS_OPEN,
    DOORS_CLOSED,
    MAINTENANCE
}

3. RequestStatus.java

public enum RequestStatus {
    PENDING,
    ASSIGNED,
    COMPLETED
}

4. Door.java

public class Door {
    private boolean open;
 
    public void open() {
        this.open = true;
    }
 
    public void close() {
        this.open = false;
    }
 
    public boolean isOpen() {
        return open;
    }
}

5. ExternalRequest.java

import java.time.Instant;
import java.util.UUID;
 
public class ExternalRequest {
    private final String id;
    private final String buildingId;
    private final int floorNumber;
    private final Direction direction;
    private final long createdAt;
    private RequestStatus status;
    private String assignedElevatorId;
 
    public ExternalRequest(String buildingId, int floorNumber, Direction direction) {
        this.id = UUID.randomUUID().toString();
        this.buildingId = buildingId;
        this.floorNumber = floorNumber;
        this.direction = direction;
        this.createdAt = Instant.now().toEpochMilli();
        this.status = RequestStatus.PENDING;
    }
 
    public void assignTo(String elevatorId) {
        this.assignedElevatorId = elevatorId;
        this.status = RequestStatus.ASSIGNED;
    }
 
    public void markCompleted() {
        this.status = RequestStatus.COMPLETED;
    }
 
    public String getId() {
        return id;
    }
 
    public String getBuildingId() {
        return buildingId;
    }
 
    public int getFloorNumber() {
        return floorNumber;
    }
 
    public Direction getDirection() {
        return direction;
    }
 
    public long getCreatedAt() {
        return createdAt;
    }
 
    public RequestStatus getStatus() {
        return status;
    }
 
    public String getAssignedElevatorId() {
        return assignedElevatorId;
    }
}

6. InternalRequest.java

import java.util.UUID;
 
public class InternalRequest {
    private final String id;
    private final String elevatorId;
    private final int destinationFloor;
    private RequestStatus status;
 
    public InternalRequest(String elevatorId, int destinationFloor) {
        this.id = UUID.randomUUID().toString();
        this.elevatorId = elevatorId;
        this.destinationFloor = destinationFloor;
        this.status = RequestStatus.PENDING;
    }
 
    public void markCompleted() {
        this.status = RequestStatus.COMPLETED;
    }
 
    public String getId() {
        return id;
    }
 
    public String getElevatorId() {
        return elevatorId;
    }
 
    public int getDestinationFloor() {
        return destinationFloor;
    }
 
    public RequestStatus getStatus() {
        return status;
    }
}

7. Elevator.java

import java.util.UUID;
 
public class Elevator {
    private final String id;
    private final String buildingId;
    private int currentFloor;
    private final int capacity;
    private int currentLoad;
    private boolean active;
    private Direction direction;
    private ElevatorState state;
    private final Door door;
 
    public Elevator(String buildingId, int currentFloor, int capacity) {
        this.id = UUID.randomUUID().toString();
        this.buildingId = buildingId;
        this.currentFloor = currentFloor;
        this.capacity = capacity;
        this.currentLoad = 0;
        this.active = true;
        this.direction = Direction.IDLE;
        this.state = ElevatorState.STOPPED;
        this.door = new Door();
    }
 
    public void moveToFloor(int targetFloor) {
        if (!active || state == ElevatorState.MAINTENANCE) {
            throw new IllegalStateException("Elevator is not available");
        }
 
        if (targetFloor == currentFloor) {
            stopAtFloor(targetFloor);
            return;
        }
 
        closeDoor();
        this.direction = targetFloor > currentFloor ? Direction.UP : Direction.DOWN;
        this.state = ElevatorState.MOVING;
        this.currentFloor = targetFloor;
        stopAtFloor(targetFloor);
    }
 
    private void stopAtFloor(int floorNumber) {
        this.currentFloor = floorNumber;
        this.direction = Direction.IDLE;
        this.state = ElevatorState.STOPPED;
        openDoor();
    }
 
    public void openDoor() {
        if (state == ElevatorState.MOVING) {
            throw new IllegalStateException("Cannot open doors while moving");
        }
        door.open();
        state = ElevatorState.DOORS_OPEN;
    }
 
    public void closeDoor() {
        door.close();
        if (state != ElevatorState.MAINTENANCE) {
            state = ElevatorState.DOORS_CLOSED;
        }
    }
 
    public void setMaintenanceMode() {
        this.active = false;
        this.direction = Direction.IDLE;
        this.state = ElevatorState.MAINTENANCE;
        closeDoor();
    }
 
    public void start() {
        this.active = true;
        this.state = ElevatorState.STOPPED;
    }
 
    public boolean canAcceptRequest() {
        return active && state != ElevatorState.MAINTENANCE && currentLoad < capacity;
    }
 
    public String getId() {
        return id;
    }
 
    public String getBuildingId() {
        return buildingId;
    }
 
    public int getCurrentFloor() {
        return currentFloor;
    }
 
    public int getCapacity() {
        return capacity;
    }
 
    public int getCurrentLoad() {
        return currentLoad;
    }
 
    public Direction getDirection() {
        return direction;
    }
 
    public ElevatorState getState() {
        return state;
    }
 
    public boolean isDoorOpen() {
        return door.isOpen();
    }
}

8. DispatchStrategy.java

import java.util.List;
 
public interface DispatchStrategy {
    Elevator selectElevator(List<Elevator> elevators, ExternalRequest request);
}

9. NearestElevatorStrategy.java

import java.util.Comparator;
import java.util.List;
 
public class NearestElevatorStrategy implements DispatchStrategy {
    @Override
    public Elevator selectElevator(List<Elevator> elevators, ExternalRequest request) {
        return elevators.stream()
                .filter(Elevator::canAcceptRequest)
                .min(Comparator.comparingInt(elevator ->
                        Math.abs(elevator.getCurrentFloor() - request.getFloorNumber())))
                .orElseThrow(() -> new IllegalStateException("No elevator available"));
    }
}

10. MovementStrategy.java

import java.util.NavigableSet;
 
public interface MovementStrategy {
    Integer nextFloor(Elevator elevator, NavigableSet<Integer> pendingFloors);
}

11. FCFSMovementStrategy.java

import java.util.NavigableSet;
 
public class FCFSMovementStrategy implements MovementStrategy {
    @Override
    public Integer nextFloor(Elevator elevator, NavigableSet<Integer> pendingFloors) {
        if (pendingFloors.isEmpty()) {
            return null;
        }
        return pendingFloors.first();
    }
}

12. ScanMovementStrategy.java

import java.util.NavigableSet;
 
public class ScanMovementStrategy implements MovementStrategy {
    @Override
    public Integer nextFloor(Elevator elevator, NavigableSet<Integer> pendingFloors) {
        if (pendingFloors.isEmpty()) {
            return null;
        }
 
        int currentFloor = elevator.getCurrentFloor();
        Direction direction = elevator.getDirection();
 
        if (direction == Direction.DOWN) {
            Integer lowerFloor = pendingFloors.floor(currentFloor);
            return lowerFloor != null ? lowerFloor : pendingFloors.first();
        }
 
        Integer higherFloor = pendingFloors.ceiling(currentFloor);
        return higherFloor != null ? higherFloor : pendingFloors.last();
    }
}

13. ElevatorController.java

import java.util.NavigableSet;
import java.util.TreeSet;
 
public class ElevatorController {
    private final Elevator elevator;
    private final NavigableSet<Integer> pendingFloors;
    private MovementStrategy movementStrategy;
 
    public ElevatorController(Elevator elevator, MovementStrategy movementStrategy) {
        this.elevator = elevator;
        this.movementStrategy = movementStrategy;
        this.pendingFloors = new TreeSet<>();
    }
 
    public void addRequest(int floorNumber) {
        pendingFloors.add(floorNumber);
    }
 
    public void step() {
        Integer nextFloor = movementStrategy.nextFloor(elevator, pendingFloors);
        if (nextFloor == null) {
            return;
        }
 
        elevator.moveToFloor(nextFloor);
        pendingFloors.remove(nextFloor);
    }
 
    public void runUntilIdle() {
        while (!pendingFloors.isEmpty()) {
            step();
        }
    }
 
    public Elevator getElevator() {
        return elevator;
    }
 
    public void setMovementStrategy(MovementStrategy movementStrategy) {
        this.movementStrategy = movementStrategy;
    }
}

14. Building.java

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
 
public class Building {
    private final String id;
    private final String name;
    private final int minFloor;
    private final int maxFloor;
    private final List<Elevator> elevators;
 
    public Building(String name, int minFloor, int maxFloor) {
        this.id = UUID.randomUUID().toString();
        this.name = name;
        this.minFloor = minFloor;
        this.maxFloor = maxFloor;
        this.elevators = new ArrayList<>();
    }
 
    public Elevator addElevator(int initialFloor, int capacity) {
        validateFloor(initialFloor);
        Elevator elevator = new Elevator(id, initialFloor, capacity);
        elevators.add(elevator);
        return elevator;
    }
 
    public void validateFloor(int floorNumber) {
        if (floorNumber < minFloor || floorNumber > maxFloor) {
            throw new IllegalArgumentException("Invalid floor: " + floorNumber);
        }
    }
 
    public String getId() {
        return id;
    }
 
    public String getName() {
        return name;
    }
 
    public List<Elevator> getElevators() {
        return Collections.unmodifiableList(elevators);
    }
}

15. ElevatorSystem.java

import java.util.HashMap;
import java.util.Map;
 
public class ElevatorSystem {
    private final Building building;
    private final DispatchStrategy dispatchStrategy;
    private final Map<String, ElevatorController> controllers;
 
    public ElevatorSystem(Building building, DispatchStrategy dispatchStrategy, MovementStrategy movementStrategy) {
        this.building = building;
        this.dispatchStrategy = dispatchStrategy;
        this.controllers = new HashMap<>();
 
        for (Elevator elevator : building.getElevators()) {
            controllers.put(elevator.getId(), new ElevatorController(elevator, movementStrategy));
        }
    }
 
    public ExternalRequest requestElevator(int floorNumber, Direction direction) {
        building.validateFloor(floorNumber);
        ExternalRequest request = new ExternalRequest(building.getId(), floorNumber, direction);
 
        Elevator elevator = dispatchStrategy.selectElevator(building.getElevators(), request);
        request.assignTo(elevator.getId());
 
        ElevatorController controller = controllers.get(elevator.getId());
        controller.addRequest(floorNumber);
        controller.runUntilIdle();
 
        request.markCompleted();
        return request;
    }
 
    public InternalRequest selectFloor(String elevatorId, int destinationFloor) {
        building.validateFloor(destinationFloor);
 
        ElevatorController controller = controllers.get(elevatorId);
        if (controller == null) {
            throw new IllegalArgumentException("Invalid elevator: " + elevatorId);
        }
 
        InternalRequest request = new InternalRequest(elevatorId, destinationFloor);
        controller.addRequest(destinationFloor);
        controller.runUntilIdle();
        request.markCompleted();
        return request;
    }
 
    public void setElevatorToMaintenance(String elevatorId) {
        ElevatorController controller = controllers.get(elevatorId);
        if (controller == null) {
            throw new IllegalArgumentException("Invalid elevator: " + elevatorId);
        }
        controller.getElevator().setMaintenanceMode();
    }
}

16. Main.java

public class Main {
    public static void main(String[] args) {
        Building building = new Building("Tech Park Tower", 0, 20);
 
        Elevator elevatorA = building.addElevator(0, 8);
        Elevator elevatorB = building.addElevator(10, 8);
        building.addElevator(20, 12);
 
        ElevatorSystem system = new ElevatorSystem(
                building,
                new NearestElevatorStrategy(),
                new FCFSMovementStrategy()
        );
 
        ExternalRequest pickup = system.requestElevator(5, Direction.UP);
        system.selectFloor(pickup.getAssignedElevatorId(), 14);
 
        system.requestElevator(18, Direction.DOWN);
        system.selectFloor(elevatorB.getId(), 2);
 
        system.setElevatorToMaintenance(elevatorA.getId());
    }
}

What I Learned

  • Dispatch strategy and movement strategy solve different problems.
  • A controller per elevator keeps pending floor state isolated.
  • The central system should coordinate flows, not own every low-level behavior.
  • Doors should be modeled explicitly because safety rules depend on door state.

Possible Improvements

  • Add async movement simulation instead of instantly jumping floors.
  • Add passenger load tracking when users enter and exit.
  • Make dispatching direction-aware so elevators already moving toward a user are preferred.
  • Add request queues for external requests that cannot be assigned immediately.
  • Add tests for invalid floors, maintenance mode, and movement strategy behavior.