Design Elevator System

Designing an Elevator System is a classic Low-Level Design problem that focuses on state management, scheduling, and real-time decision making. The goal is to efficiently move passengers between floors while minimizing wait time and ensuring safety.

A well-designed system separates domain modeling, request handling, scheduling logic, and execution. This separation keeps the system maintainable and extensible for future enhancements like smart routing or load balancing.

An elevator system consists of multiple elevators, each serving a building with multiple floors. Users generate requests either from floors (hall requests) or inside elevators (car requests). The system must assign elevators efficiently and process requests in an optimal order.

Domain Modeling

Basic Enums

Elevators operate in different directions and states.
enum Direction {
    UP,
    DOWN,
    IDLE
}

enum ElevatorState {
    MOVING,
    STOPPED,
    IDLE
}

Request

Requests can originate from outside or inside the elevator.
class Request {

    private final int floor;
    private final Direction direction;

    public Request(int floor, Direction direction) {
        this.floor = floor;
        this.direction = direction;
    }

    public int getFloor() {
        return floor;
    }

    public Direction getDirection() {
        return direction;
    }
}

Elevator

Each elevator maintains its current state and a queue of target floors.
import java.util.PriorityQueue;

class Elevator {

    private final int id;
    private int currentFloor;
    private Direction direction;
    private ElevatorState state;

    private final PriorityQueue<Integer> upQueue = new PriorityQueue<>();
    private final PriorityQueue<Integer> downQueue =
            new PriorityQueue<>((a, b) -> b - a);

    public Elevator(int id) {
        this.id = id;
        this.currentFloor = 0;
        this.direction = Direction.IDLE;
        this.state = ElevatorState.IDLE;
    }

    public void addRequest(int floor) {
        if (floor > currentFloor) {
            upQueue.offer(floor);
        } else if (floor < currentFloor) {
            downQueue.offer(floor);
        }
    }

    public void move() {

        if (direction == Direction.UP) {
            processUp();
        } else if (direction == Direction.DOWN) {
            processDown();
        } else {
            decideNextDirection();
        }
    }

    private void processUp() {
        if (!upQueue.isEmpty()) {
            state = ElevatorState.MOVING;
            currentFloor = upQueue.poll();
        } else {
            direction = Direction.DOWN;
        }
    }

    private void processDown() {
        if (!downQueue.isEmpty()) {
            state = ElevatorState.MOVING;
            currentFloor = downQueue.poll();
        } else {
            direction = Direction.UP;
        }
    }

    private void decideNextDirection() {
        if (!upQueue.isEmpty()) {
            direction = Direction.UP;
        } else if (!downQueue.isEmpty()) {
            direction = Direction.DOWN;
        }
    }

    public int getCurrentFloor() {
        return currentFloor;
    }

    public Direction getDirection() {
        return direction;
    }
}
These two queues represent how an elevator organizes its upcoming stops based on direction.
 private final PriorityQueue upQueue = new PriorityQueue<>(); 
This is a min-heap, which always gives the smallest floor first. It is used when the elevator is moving upwards, so it serves the nearest higher floors in ascending order (e.g., 3 β†’ 5 β†’ 7).
 private final PriorityQueue downQueue = new PriorityQueue<>((a, b) -> b - a); 
This is a max-heap, which always gives the largest floor first. It is used when the elevator is moving downwards, so it serves the nearest lower floors in descending order (e.g., 10 β†’ 8 β†’ 6).

Together, these queues ensure the elevator continues smoothly in one direction, picking the next closest stop, instead of jumping between floors randomly.

Elevator System

The system manages multiple elevators and assigns requests.
import java.util.List;

class ElevatorSystem {

    private final List<Elevator> elevators;

    public ElevatorSystem(List<Elevator> elevators) {
        this.elevators = elevators;
    }

    public Elevator assignElevator(Request request) {

        Elevator best = null;
        int minDistance = Integer.MAX_VALUE;

        for (Elevator elevator : elevators) {
            int distance = Math.abs(elevator.getCurrentFloor() - request.getFloor());

            if (distance < minDistance) {
                minDistance = distance;
                best = elevator;
            }
        }

        return best;
    }
}

Scheduling Strategy

The core challenge in an elevator system lies in deciding which elevator should serve a request while minimizing wait time and unnecessary movement. A naive approach assigns the nearest elevator, but this often leads to inefficient routing because it ignores important factors such as the current direction of movement, existing scheduled stops, and load on the elevator.

A more practical strategy considers whether an elevator is already moving toward the requested floor and whether it can accommodate the request without significant deviation.

To model this behavior, each elevator maintains two separate queues: an up queue that serves floors in ascending order and a down queue that serves floors in descending order. This separation ensures that while moving in one direction, the elevator processes all relevant requests efficiently before switching direction.

As a result, the system avoids frequent direction changes, reduces travel time, and closely mimics how real-world elevators operate.

When a user presses a button, the system generates a request containing the floor and direction. This request is then evaluated by the ElevatorSystem, which selects the most suitable elevator based on proximity and movement direction.

Once an elevator is assigned, the request is added to its appropriate queue, and the elevator processes it as part of its existing schedule. This flow ensures that requests are handled in a structured manner without disrupting ongoing operations.

In a real-world scenario, multiple users may generate requests at the same time, leading to concurrent interactions with the system. To maintain correctness, the system must ensure that request assignment is thread-safe, preventing multiple elevators from being assigned the same request.

It must also avoid duplicate scheduling and ensure that the internal state of each elevator remains consistent even under high load.

These guarantees can be achieved using synchronization mechanisms or concurrent data structures, depending on the scale of the system. The key is to ensure that while multiple requests are processed in parallel, the system’s behavior remains predictable and free from race conditions.

Conclusion

Designing an elevator system highlights how to model real-time scheduling problems using clean abstractions. The focus should be on efficient request handling, minimal movement, and clear separation of responsibilities.

A good design balances simplicity with extensibility, ensuring that the system performs well today while adapting to future requirements.
Nagesh Chauhan
Nagesh Chauhan
Principal Engineer | Java Β· Spring Boot Β· Python Β· Microservices Β· AI/ML

Principal Engineer with 14+ years of experience in designing scalable systems using Java, Spring Boot, and Python. Specialized in microservices architecture, system design, and machine learning.

Share this Article

πŸ’¬ Comments

Join the discussion