SevyDevy

Guide · System design

LLD: Design an Elevator System

20 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • Elevator low level design is the class-level plan for a lift controller's software: which classes exist, what each one owns, and the rules that pick where each car goes next.
  • Its heart is scheduling, the rule for which floor comes next. The standard answer is the elevator algorithm, also called SCAN: keep going one way, stop for riders on the way, and turn round only at the end of the building.
  • LOOK is a SCAN variant that turns round at the last requested floor instead of the end of the building. In Java, a sorted set called TreeSet finds that next floor in one call.
  • The state pattern gives each mode of the car, such as Idle, MovingUp, MovingDown and DoorsOpen, its own small class. A separate dispatcher picks which car answers each up or down button press on a floor.
  • Beginners can stop after the state machine diagram, the picture of the car's modes and the moves between them. Engineers get a complete Java controller that runs, the turning-point rule whose absence made our test car shuttle forever, and a door-timing bug our random test caught.

What goes wrong when a lift serves calls in the order they were pressed?

The car zigzags. It drives past people who are waiting, carries riders the wrong way and makes everyone wait longer, because the oldest call is rarely the next floor on the car's path.

Elevator low level design fixes this by splitting the controller into small classes with one job each, and by choosing the order of stops with a proper scheduling rule.

Picture a common setup. A residential tower has a ground floor, numbered 0, and 20 floors above it, served by two cars. The setup is made up, but the three failures below are the classic ones.

The tower's first controller kept one plain queue of calls, in the order they were pressed, and always drove to the oldest one. A call is any request for a car: a button pressed on a floor, or a floor number pressed inside the car.

  • A car going down from floor 18 drove straight past a resident waiting on floor 15, because an older call on floor 2 came first in the queue.
  • A rider going up to floor 14 watched the car turn round and go down, because someone had pressed a call on floor 3 a little earlier.
  • A child pressed 9 three times, and the queue held three separate trips to floor 9.

None of these is a motor problem. They come from the order of the queue, and from one class trying to do everything. This guide rebuilds that controller step by step, and every Java sample in it compiles and runs.

What is elevator low level design?

Elevator low level design is the detailed plan of classes, their fields and methods, and how they work together to move cars and answer calls. It sits one level below the big picture of the building.

Low level design, often shortened to LLD, means designing the code inside one program. You decide which classes exist, what each one owns and how they call each other. High level design instead draws servers, databases and networks.

The elevator is a common LLD interview question. It looks like a toy, but it tests three skills at once: modelling objects, choosing an algorithm, and handling state that changes every second.

The words you need first

Five words come up in every section. The table gives each a plain meaning and an example from the tower.

Five terms used throughout this guide, with examples from the two-car tower
TermPlain meaningExample in the tower
Hall callA press of the up or down button on a floor, outside the liftA resident on floor 15 presses down
Car callA press of a floor number inside a carThat resident steps in and presses 0
SchedulingThe order in which one car serves its own stopsFloor 9 before floor 14 on the way up
DispatchingChoosing which car answers a new hall callCar B gets floor 15 because it is already close
StateThe mode a car is in right nowIdle, moving up, moving down or doors open

The same button press can mean different things in different states. Pressing the door-open button while the car is moving must do nothing, and that is why state gets its own section later.

What are the requirements for an elevator system design?

The requirements are the rules the system must follow, split into what it must do and how well it must do it. Write them down before any class, because they decide every later choice.

Functional requirements are the things the system must do. For the tower they are these.

  • Accept hall calls with a direction, up or down, from every floor.
  • Accept car calls for any floor from inside each car.
  • Choose which car answers each hall call.
  • Move each car floor by floor, stop at requested floors, and open and close the doors.
  • Handle special modes: maintenance, where a technician drives the car, and fire recall, where cars go to a set floor and stop answering calls.

Non-functional requirements say how well the system must behave. No call may wait forever. A rider must never be carried away from their floor. Adding a third car, or a new scheduling rule, must not need a rewrite.

Real lifts also have safety devices that your scheduling code does not replace. Door interlocks stop a car moving unless the doors are closed. An overspeed device clamps brake shoes onto the rails if the car goes down faster than its design speed.

Your design decides where the car goes next. Treat interlocks and brakes as hardware signals you read, not as code you write.

What classes do you need for an elevator system?

You need a Building that receives button presses, one Elevator per car with its own Scheduler, and a Dispatcher that picks a car for each hall call. Each mode of a car gets one State class.

Each class owns one job. That is the single responsibility idea from the SOLID principles, a well-known set of five object-oriented design rules.

  • Building holds the cars and the Dispatcher. Every button press enters the system here, as an event.
  • Dispatcher receives every hall call and gives it to the car with the lowest cost.
  • Elevator is one car. It knows its floor, its direction, its door sensors and its current state.
  • Scheduler belongs to one car. It keeps that car's stops and answers one question: should the car stop here?
  • ElevatorState is an interface, a list of methods with no body, with one class per mode: Idle, MovingUp, MovingDown and DoorsOpen.
  • HallCall is a small value object, a class that only carries data: a floor and a direction.

In the diagram, follow a hall call from the floor button to the car. Notice that the Dispatcher never moves a car. It only hands a stop to one car's Scheduler, and the car's state does the moving.

Classes in the two-car controller

The Dispatcher only picks a car. Each car's Scheduler and current state decide how that car moves.

Diagram connections:

  • Floor buttons to Building (Hall call)
  • Car panel to Building (Car call)
  • Building to Dispatcher (Find a car)
  • Dispatcher to Scheduler (Add stop)
  • Building to Scheduler (Car call, no dispatch)
  • Elevator (one per car) to Scheduler (Stop here?)
  • Elevator (one per car) to ElevatorState (Delegates each tick)
  • Floor buttons: Hall calls with up or down
  • Building: Turns every press into an event
  • Dispatcher: Gives the call to the cheapest car
  • Elevator (one per car): Floor, direction, door sensors, state
  • Scheduler: Three sorted sets of stops
  • ElevatorState: Idle, MovingUp, MovingDown, DoorsOpen
  • Car panel: Car calls from inside

Why give each car its own Scheduler instead of one big list in the Dispatcher? Once a car accepts a stop, only that car's position and direction matter. Keeping stops per car also makes a third car one more object, not a rewrite.

How does the elevator scheduling algorithm work?

The elevator algorithm keeps the car moving in its current direction and stops at every requested floor on the way. In its textbook form, SCAN, the car turns round only at the end of the shaft.

The same idea is used to order requests to a hard disk, whose arm moves across tracks much like a car moves across floors. The operating systems textbook OSTEP notes that SCAN is sometimes called the elevator algorithm for exactly this reason.

Picture a shikara boatman on Dal Lake, picking up guests from a long row of houseboats. He rows along the row in one sweep and stops wherever a guest waves. A guest behind him waits for the return sweep.

The mapping is direct: the row is the shaft, the houseboats are floors, a wave is a call and the sweep is the car's direction. Where the picture breaks: a boat can turn anywhere at once, while a lift must stop at a floor and open its doors first.

There are four common ways to order one car's stops. Each answers the same question differently: which floor next?

  • First come, first served. Go to the oldest call first. It sounds fair, but the car zigzags, exactly as in the tower.
  • Nearest first. Go to the closest requested floor. Trips are short, but a steady stream of nearby calls can keep a far floor waiting forever. Disk scheduling calls this starvation.
  • SCAN. Sweep all the way to the top floor, then all the way down, stopping at requested floors on the way.
  • LOOK. Like SCAN, but turn round at the last requested floor in that direction instead of travelling to the end.

One worked example

Say a car is at floor 6, going up with a rider who pressed 14. Stops at floors 2, 9 and 4 are then requested, in that order. Assume all four stops are known while the car is at floor 6.

Pause and predict: which rule travels the fewest floors, and what does that rule do to the rider going to 14? The table counts the floors for each rule.

Four scheduling rules on one car at floor 6 going up, with stops 14, 2, 9 and 4 (checked by a script, see the ledger)
RuleOrder of stopsFloors travelledMain risk
First come, first served14, 2, 9, 432Zigzags and passes waiting people
Nearest first4, 2, 9, 1416Carries the rider for 14 down first; far floors can starve
SCAN9, 14, top floor 20, 4, 232Wasted trip to the top
LOOK9, 14, 4, 220Four floors more than nearest first

Nearest first wins on distance here. But its first move takes the rider bound for floor 14 down to floor 4. OSTEP jokes that the elevator algorithm, used in real life, "prevents fights from taking place on elevators".

LOOK costs four extra floors in this example. In return, nobody inside the car is carried away from their floor. And because the car never turns while stops remain ahead, no floor waits forever. In our one-car test, no passenger waited for more than 40 floors of travel, two full sweeps of this tower.

How do you implement the LOOK algorithm in Java?

Keep the car's stops in a sorted set. Going up, ask it for the nearest stop at or above the current floor; going down, at or below it. Java's TreeSet does that search for you.

A TreeSet keeps its items sorted and drops duplicates. Its ceiling method returns the smallest item greater than or equal to a value, and floor returns the largest item less than or equal to it. Both return null, Java's value for no object, when no such item exists.

The Java SE 21 documentation guarantees log(n) time for add, remove and contains, so the cost grows with the logarithm of the number of stops. Save this as LookDemo.java.

java
import java.util.TreeSet;

class LookDemo {
    public static void main(String[] args) {
        LookScheduler car = new LookScheduler();
        for (int f : new int[] {14, 2, 9, 4}) car.addStop(f);
        int floor = 6, travelled = 0;
        Direction dir = Direction.UP;
        StringBuilder order = new StringBuilder();
        Integer next;
        while ((next = car.nextStop(floor, dir)) != null) {
            dir = next >= floor ? Direction.UP : Direction.DOWN; // turn when needed
            travelled += Math.abs(next - floor);
            floor = next;
            car.arrivedAt(floor);
            order.append(floor).append(' ');
        }
        System.out.println("Visit order: " + order + "| floors travelled: " + travelled);
    }
}

enum Direction { UP, DOWN, IDLE }

class LookScheduler {
    private final TreeSet<Integer> stops = new TreeSet<>();

    void addStop(int floor)   { stops.add(floor); }
    void arrivedAt(int floor) { stops.remove(floor); }

    // Next floor to visit, or null when there is nothing to do.
    Integer nextStop(int current, Direction dir) {
        Integer above = stops.ceiling(current); // smallest stop >= current
        Integer below = stops.floor(current);   // largest stop <= current
        if (dir == Direction.DOWN) return below != null ? below : above;
        return above != null ? above : below;
    }
}

Run it with javac LookDemo.java and then java LookDemo, or in one step with java LookDemo.java. We ran both ways on Java 21, and the one-step way on Java 17 too. You should see exactly this line.

text
Visit order: 9 14 4 2 | floors travelled: 20

What just happened? Going up from 6, ceiling found 9, then 14. Above 14 there was nothing, so nextStop fell back to the largest stop below, which was 4, and the loop turned the car round. That is LOOK, and it matches the table.

Two small details do real work. Because the set drops duplicates, the child who pressed 9 three times creates only one stop. Because ceiling includes the current floor, a stop on the car's own floor comes back first, so the doors simply open.

How does the state pattern work in an elevator system?

The state pattern gives each mode of the car its own small class, and the car hands every tick to whichever state object is current. Changing mode means swapping that object.

Without it, the Elevator class grows one long if-else on a mode field. Every method checks it: if moving, ignore the door-open button; if the doors are open, do not start the motor. Each new mode means editing every one of those checks.

The pattern has three parts. The context is the object whose behaviour changes, here the Elevator. The state interface lists what every mode must handle. Each concrete state, like DoorsOpen, handles those actions its own way and decides which state comes next.

The diagram shows the four states. A tick is one control cycle, a short fixed interval in which the controller updates every car once. Start at Idle, and notice that every change of direction goes through the stops check.

The car's state machine

Every turn goes through the stops check: after the doors close, or when the stops ahead were moved to another car. Only DoorsOpen closes the doors.

Diagram connections:

  • Idle to MovingUp (Stop above)
  • Idle to MovingDown (Stop below)
  • Idle to DoorsOpen (Stop on this floor)
  • MovingUp to DoorsOpen (Reached a stop)
  • MovingDown to DoorsOpen (Reached a stop)
  • DoorsOpen to Any stops left? (Doors closed)
  • MovingUp to Any stops left? (Nothing left above)
  • MovingDown to Any stops left? (Nothing left below)
  • Any stops left? to MovingUp (Yes, above)
  • Any stops left? to MovingDown (Yes, below)
  • Any stops left? to Idle (No)
  • Idle: Doors closed, no stops waiting
  • MovingUp: One floor per tick, stops when told
  • MovingDown: One floor per tick, stops when told
  • DoorsOpen: Holds while the door is blocked
  • Any stops left?: Runs after the doors close

Adding a mode is now small. Maintenance becomes one new class that ignores hall calls and moves only on a technician's command, and no existing state needs to change.

How do you build a complete elevator controller in Java?

Split the controller into five blocks: a Scheduler with three sets of stops, four state classes, the Elevator itself, a Dispatcher, and a Building that turns presses into events. Paste the five blocks in order into one file called Lift.java.

The code needs Java 16 or newer because it uses a record, a short class that only holds data. We compiled and ran it on OpenJDK 21.0.12 and 17.0.20 as of 19 October 2026; javac with release 15 rejects the record.

Three sets of stops and the turning-point rule

The Scheduler keeps car calls, up hall calls and down hall calls in three TreeSets. A car going up stops for car calls and up calls. Down calls wait for the downward sweep, like the guest behind the boatman.

java
import java.util.*;

enum Direction { UP, DOWN, IDLE }

record HallCall(int floor, Direction dir) {}

// One car's stops, kept in three sorted sets.
class Scheduler {
    private final TreeSet<Integer> carStops = new TreeSet<>();  // pressed inside the car
    private final TreeSet<Integer> upCalls = new TreeSet<>();   // up button on a floor
    private final TreeSet<Integer> downCalls = new TreeSet<>(); // down button on a floor

    void addCarStop(int floor) { carStops.add(floor); }
    void addHallCall(int floor, Direction dir) {
        (dir == Direction.UP ? upCalls : downCalls).add(floor);
    }

    boolean anyAt(int f) {
        return carStops.contains(f) || upCalls.contains(f) || downCalls.contains(f);
    }
    boolean anyAbove(int f) {
        return carStops.higher(f) != null || upCalls.higher(f) != null || downCalls.higher(f) != null;
    }
    boolean anyBelow(int f) {
        return carStops.lower(f) != null || upCalls.lower(f) != null || downCalls.lower(f) != null;
    }

    // Should a car moving in direction dir stop at floor f?
    boolean shouldStop(int f, Direction dir) {
        if (carStops.contains(f)) return true;
        if (dir == Direction.UP) return upCalls.contains(f) || (downCalls.contains(f) && !anyAbove(f));
        return downCalls.contains(f) || (upCalls.contains(f) && !anyBelow(f));
    }

    // Clear what a stop at f serves. Returns the direction the car leaves in.
    Direction serve(int f, Direction dir) {
        carStops.remove(f);
        if (dir != Direction.DOWN && upCalls.remove(f)) return Direction.UP;
        if (dir != Direction.UP && downCalls.remove(f)) return Direction.DOWN;
        if (dir == Direction.UP && !anyAbove(f) && downCalls.remove(f)) return Direction.DOWN;
        if (dir == Direction.DOWN && !anyBelow(f) && upCalls.remove(f)) return Direction.UP;
        return dir;
    }

    // Where a car moving in dir turns round, if it must also reach floor f.
    int turningFloor(int at, Direction dir, int f) {
        List<Integer> all = new ArrayList<>(List.of(at, f));
        all.addAll(carStops); all.addAll(upCalls); all.addAll(downCalls);
        return dir == Direction.UP ? Collections.max(all) : Collections.min(all);
    }

    int size() { return carStops.size() + upCalls.size() + downCalls.size(); }

    // Hand back the hall calls (for another car) and drop the car calls.
    List<HallCall> drainHallCalls() {
        List<HallCall> out = new ArrayList<>();
        for (int f : upCalls) out.add(new HallCall(f, Direction.UP));
        for (int f : downCalls) out.add(new HallCall(f, Direction.DOWN));
        upCalls.clear(); downCalls.clear(); carStops.clear();
        return out;
    }
}

One case needs care. Say the car is going up and the highest request anywhere is a down call on floor 15. Nobody is going further up, so floor 15 is where the car turns. It must stop there on the way up and leave going down.

Pause and predict: what happens if shouldStop simply ignores down calls on the way up? With only that one call, the test car still got there. It reached floor 15 without stopping, found nothing above, went idle and only then opened, two ticks late.

With calls at both ends, it broke. A down call on floor 20 and an up call on floor 0 made the test car shuttle between the two floors without ever opening its doors. In our random test, that version stranded 225 of 12,000 passengers.

The reason is that a moving car only goes idle when nothing is left anywhere. With a request behind it at each end, it always had somewhere to go and never stopped. The two turning-point clauses in shouldStop, the ones that call anyAbove and anyBelow, are the fix.

The four states

Each state handles one tick. MovingUp and MovingDown move one floor and ask the Scheduler whether to stop. DoorsOpen is the only class that closes the doors, and it refuses while the door sensor is blocked or the car is overloaded.

java
interface ElevatorState { void onTick(Elevator car); }

class Idle implements ElevatorState {
    public void onTick(Elevator car) {
        if (car.scheduler().anyAt(car.floor())) {          // a call on this floor
            car.arrive(Direction.IDLE);
        } else {
            car.setState(car.pickNextState());
        }
    }
}

class MovingUp implements ElevatorState {
    public void onTick(Elevator car) {
        if (!car.scheduler().anyAbove(car.floor())) {     // stop moved to another car
            car.setState(car.pickNextState());
            return;
        }
        car.moveOneFloor(+1);
        if (car.scheduler().shouldStop(car.floor(), Direction.UP)) car.arrive(Direction.UP);
    }
}

class MovingDown implements ElevatorState {
    public void onTick(Elevator car) {
        if (!car.scheduler().anyBelow(car.floor())) {
            car.setState(car.pickNextState());
            return;
        }
        car.moveOneFloor(-1);
        if (car.scheduler().shouldStop(car.floor(), Direction.DOWN)) car.arrive(Direction.DOWN);
    }
}

class DoorsOpen implements ElevatorState {
    private int ticksLeft = 2;   // doors stay open for two ticks
    private int heldTicks = 0;
    private int restarts = 0;    // late presses served at this stop

    public void onTick(Elevator car) {
        if (car.doorBlocked || car.overloaded) {          // never close on a person
            if (++heldTicks == 30) car.log("ALERT: doors held for 30 ticks");
            return;
        }
        // A late press for this floor: serve it and restart the door timer,
        // at most 3 times. Later presses wait for the car's next visit.
        if (restarts < 3) {
            int before = car.scheduler().size();
            car.setDirection(car.scheduler().serve(car.floor(), car.direction()));
            if (car.scheduler().size() < before) { ticksLeft = 2; restarts++; }
        }
        if (--ticksLeft > 0) return;
        car.log("closes doors");
        car.setState(car.pickNextState());
    }
}

The late-press lines in DoorsOpen came from a bug our own random test found. Our first version served a press for the current floor on the same tick that it closed the doors. The call was cleared, but nobody could board.

That version stranded between 18 and 44 passengers in each batch of 12,000. Restarting the door timer whenever a late press is served fixed it, and the same test then delivered everyone.

A later test found the opposite problem. With a new lobby press every tick, the doors never closed, and a down call on floor 10 waited for all 2,000 ticks of the test.

So DoorsOpen now restarts its timer at most 3 times per stop, and later presses wait for the next visit. Floor 10 was then served at tick 14, and the random test still delivered everyone.

The car itself

The Elevator is the context. It holds the current state, the direction and the door sensors, and it owns the stops check from the diagram, called pickNextState. The guard in moveOneFloor throws an exception rather than drive past the top or bottom floor.

java
class Elevator {
    final String name;
    private final int topFloor;
    private int floor;
    private Direction direction = Direction.IDLE;
    private ElevatorState state = new Idle();
    private final Scheduler scheduler = new Scheduler();
    boolean doorBlocked, overloaded, inService = true;
    static int clock = 0;   // the demo's tick counter

    Elevator(String name, int floor, int topFloor) {
        this.name = name; this.floor = floor; this.topFloor = topFloor;
    }

    int floor() { return floor; }
    Direction direction() { return direction; }
    void setDirection(Direction d) { direction = d; }
    Scheduler scheduler() { return scheduler; }
    boolean doorsOpen() { return state instanceof DoorsOpen; }
    void setState(ElevatorState next) { state = next; }
    void tick() { if (inService) state.onTick(this); }
    void log(String msg) { System.out.printf("t=%02d %s %s%n", clock, name, msg); }

    void moveOneFloor(int step) {
        int next = floor + step;
        if (next < 0 || next > topFloor) throw new IllegalStateException(name + " cannot reach " + next);
        floor = next;
    }

    // Stop here: clear the stops this floor serves, then open the doors.
    void arrive(Direction movingDir) {
        direction = scheduler.serve(floor, movingDir);
        log("opens doors at " + floor + ", leaving " + direction);
        state = new DoorsOpen();
    }

    // The "any stops left?" check from the state diagram.
    ElevatorState pickNextState() {
        if (direction != Direction.DOWN && scheduler.anyAbove(floor)) { direction = Direction.UP; return new MovingUp(); }
        if (scheduler.anyBelow(floor)) { direction = Direction.DOWN; return new MovingDown(); }
        if (scheduler.anyAbove(floor)) { direction = Direction.UP; return new MovingUp(); }
        direction = Direction.IDLE;
        return new Idle();
    }
}

Note the order inside pickNextState. A car keeps its direction while anything is left ahead, and only then looks behind it. That rule, with the direction checks in the Scheduler, is what stops riders being carried away from their floors.

How do you choose which elevator answers a hall call?

The Dispatcher gives each car a cost for the call, an estimate of how many floors it must travel to get there, and picks the cheapest car. The cost rule sits in its own class, so you can change it without touching the cars.

Go back to the lake. Two boatmen work the same row of houseboats. When a new guest waves, you would not send the boatman who just rowed past her. You send the one already heading her way, or the one resting nearby.

The cost rule used here has three cases. It is one reasonable rule for this tower, not an industry standard.

  1. Car already heading there

    If the car is moving toward the call, in the call's direction, the cost is the number of floors between them. It will pass that floor anyway.

  2. Car idle

    The cost is the distance plus 2. An idle car is free, but waking it should lose to a car that is already passing by.

  3. Car moving away, or the wrong way

    The cost is the distance out to where the car turns round, plus the distance from there back to the call.

  4. Pick the lowest cost

    The Dispatcher adds the hall call to that car's Scheduler. Ties go to the car with fewer stops, to spread the work.

In code, the cost rule is a DispatchStrategy object. That is the strategy pattern: a family of interchangeable rules, each in its own class, that the Dispatcher can swap at run time with setStrategy.

java
interface DispatchStrategy { int cost(Elevator car, int floor, Direction dir); }

class NearestCarStrategy implements DispatchStrategy {
    public int cost(Elevator car, int floor, Direction dir) {
        int at = car.floor();
        Direction moving = car.direction();
        if (moving == Direction.IDLE) return Math.abs(floor - at) + 2;  // wake-up penalty
        boolean ahead = moving == Direction.UP ? floor > at : floor < at;
        if (floor == at && car.doorsOpen()) ahead = true;
        if (ahead && dir == moving) return Math.abs(floor - at);       // passes it anyway
        int turn = car.scheduler().turningFloor(at, moving, floor);
        return Math.abs(turn - at) + Math.abs(turn - floor);           // out to the turn and back
    }
}

class Dispatcher {
    private final List<Elevator> cars;
    private DispatchStrategy strategy = new NearestCarStrategy();

    Dispatcher(List<Elevator> cars) { this.cars = cars; }
    void setStrategy(DispatchStrategy s) { strategy = s; }

    void assign(int floor, Direction dir) {
        Elevator best = null;
        int bestCost = Integer.MAX_VALUE;
        for (Elevator car : cars) {
            if (!car.inService) continue;
            int c = strategy.cost(car, floor, dir);
            if (c < bestCost || (c == bestCost && car.scheduler().size() < best.scheduler().size())) {
                best = car; bestCost = c;
            }
        }
        if (best == null) throw new IllegalStateException("no car in service");
        best.scheduler().addHallCall(floor, dir);
        best.log("gets hall call " + floor + " " + dir + " (cost " + bestCost + ")");
    }

    void takeOutOfService(Elevator car) {
        car.inService = false;
        car.log("out of service");
        for (HallCall call : car.scheduler().drainHallCalls()) assign(call.floor(), call.dir());
    }
}

The takeOutOfService method covers a car that stops working. Its hall calls go back through assign to the other cars, and its car calls are dropped.

The diagram traces the first hall call of the demo below. Each box is one step, and its note names who talks to whom.

One hall call from button to open doors

The down button on floor 15 goes in as an event. Idle car B, three floors away, costs 5 and wins over car A at floor 0.

Diagram connections:

  • Down pressed on floor 15 to Event joins the queue (Hall call)
  • Event joins the queue to Cost each car (Control thread)
  • Cost each car to Down call to car B (A costs 17, B costs 5)
  • Down call to car B to B moves down 3 floors (Next ticks)
  • B moves down 3 floors to B opens at floor 15 (shouldStop is true)
  • Down pressed on floor 15: Floor button → Building
  • Event joins the queue: Building → event queue
  • Cost each car: Dispatcher → cars A and B
  • Down call to car B: Dispatcher → B's Scheduler
  • B moves down 3 floors: MovingDown → B's motor
  • B opens at floor 15: DoorsOpen → B's doors

Some tall, busy buildings go further, with destination dispatch. People enter their destination floor on a keypad or touch screen before boarding, and the system groups people going to the same floors into the same car.

Destination dispatch has its own catch. In most cases the system cannot tell a group from one person, so a car may stop for more people than it can carry. The fixes include a load sensor and a group button on the keypad.

Groups of cars also switch modes by time of day. In up-peak mode, for people arriving in the morning, cars return to the lobby. They leave one at a time once they reach a set load or their doors have been open for a set time.

How do you run the elevator demo and check the output?

The last block is the Building and a scripted demo. Buttons never touch a Scheduler directly. They put an event on a queue, and one control thread, a single line of execution in the program, runs the events and then ticks every car.

java
// Buttons only add events. One control thread runs them, then ticks every car.
class Building {
    final Elevator a = new Elevator("A", 0, 20), b = new Elevator("B", 18, 20);
    final Dispatcher dispatcher = new Dispatcher(List.of(a, b));
    private final java.util.concurrent.BlockingQueue<Runnable> events =
            new java.util.concurrent.LinkedBlockingQueue<>();

    void pressHall(int floor, Direction dir) { events.add(() -> dispatcher.assign(floor, dir)); }
    void pressInCar(Elevator car, int floor) { events.add(() -> car.scheduler().addCarStop(floor)); }
    void reportFault(Elevator car) { events.add(() -> dispatcher.takeOutOfService(car)); }

    void tick() {                       // one control cycle
        Runnable e;
        while ((e = events.poll()) != null) e.run();
        a.tick(); b.tick();
        Elevator.clock++;
    }
}

class LiftDemo {
    public static void main(String[] args) {
        Building t = new Building();
        for (int tick = 0; tick <= 40; tick++) {
            switch (tick) {
                case 0 -> { t.pressHall(15, Direction.DOWN); t.pressHall(0, Direction.UP); }
                case 2 -> t.pressInCar(t.a, 14);              // rider who got in at 0
                case 4 -> t.pressInCar(t.b, 0);               // rider who got in at 15
                case 5 -> t.pressHall(9, Direction.UP);
                case 7 -> t.pressHall(4, Direction.DOWN);
                case 10 -> t.pressHall(17, Direction.DOWN);   // above A's last stop
                case 12 -> t.reportFault(t.b);
            }
            t.tick();
        }
        System.out.println("A at " + t.a.floor() + ", B at " + t.b.floor());
    }
}

Compile and run it with these two commands. On Java 17 and 21 we got exactly the output below, line for line.

bash
javac Lift.java
java LiftDemo
text
t=00 B gets hall call 15 DOWN (cost 5)
t=00 A gets hall call 0 UP (cost 2)
t=00 A opens doors at 0, leaving UP
t=02 A closes doors
t=03 B opens doors at 15, leaving DOWN
t=05 A gets hall call 9 UP (cost 7)
t=05 B closes doors
t=07 B gets hall call 4 DOWN (cost 10)
t=10 A gets hall call 17 DOWN (cost 10)
t=11 A opens doors at 9, leaving UP
t=12 B out of service
t=12 A gets hall call 4 DOWN (cost 21)
t=13 A closes doors
t=18 A opens doors at 14, leaving UP
t=20 A closes doors
t=23 A opens doors at 17, leaving DOWN
t=25 A closes doors
t=38 A opens doors at 4, leaving DOWN
t=40 A closes doors
A at 4, B at 9

What just happened? At t=00, idle car B at floor 18 cost 5 for the floor 15 call, against 17 for car A at floor 0. At t=07, B was heading down from floor 14, so the floor 4 down call cost it 10 against 20 for A.

At t=10, A was going up, so the floor 17 down call became its turning point. At t=12, B reported a fault. Its floor 4 call moved to A at cost 21, and A served it at t=38 after turning at floor 17.

The rider inside B, bound for floor 0, lost their car call. That is deliberate: they are inside a broken car and need a technician, not a schedule.

A demo shows one path. We also ran a random test on the same classes: 200 runs of 60 passengers each, with random floors. With two cars and a new passenger every 6 ticks on average, every passenger was delivered, no rider moved away from their floor, and the longest wait was 70 ticks.

What edge cases break an elevator design?

The cases that break most designs are blocked doors, overload, many presses at once, special modes and a car that stops working. Each needs one clear owner in your classes.

  • Blocked door. DoorsOpen holds the doors while the sensor sees something. In our test, it raised the alert at 30 held ticks and closed normally once unblocked.
  • Overload. DoorsOpen refuses to close until enough weight leaves. A fuller design also makes the Dispatcher skip a car that is nearly full.
  • Presses at the same moment. Button threads must not write to a TreeSet directly; the documentation says it is not synchronized and must be locked externally.
  • Repeated presses. The sorted set ignores duplicates, so pressing 9 five times adds one stop. Our test confirmed a Scheduler size of 1.
  • Endless late presses. DoorsOpen serves at most 3 late presses per stop and then closes, so one busy floor cannot hold a car forever.
  • Top and bottom floors. moveOneFloor throws an exception instead of moving the car to floor 21 in a 0 to 20 building.
  • Fire recall. After an alarm, cars go to a recall floor, stop with their doors open and stop answering calls. If the alarm is on that floor, they use an alternate floor.
  • A car out of service. The Dispatcher stops sending it calls and moves its waiting hall calls to the other cars, as the demo showed.

The thread case is easy to underestimate. We had 8 threads each add 10,000 different numbers to one plain TreeSet, 20 times, on a 2-CPU machine. Only 2 of the 20 runs ended with all 80,000 numbers.

Sixteen runs lost numbers, and two of those also threw an exception. Two more runs hung, with a writer thread still running after 5 seconds. The same work through a LinkedBlockingQueue, drained by one thread, was correct in all 20 runs. Results change from run to run: in two later batches of 12 runs, 4 and 7 were correct, and none hung.

That is why the Building uses a queue. The BlockingQueue documentation states that its implementations are thread-safe, so many button threads can add events while one control thread is the only writer of every Scheduler.

How do you explain elevator low level design in a system design interview?

Explain it in layers: requirements, classes, the scheduling rule for one car, the states, the dispatcher across cars, then the edge cases. Interviewers want to hear each decision and its reason, not a finished program.

  1. Clarify the building

    Ask about floors, cars, directional hall buttons and special modes. Write the functional and non-functional requirements as two short lists.

  2. Name the classes

    Building, Dispatcher, Elevator, Scheduler, ElevatorState and HallCall. Give each one a single job in one sentence.

  3. Pick a scheduling rule for one car

    Compare first come first served, nearest first and LOOK on a tiny example. Choose LOOK and say why: no wrong-way rides and no starved floors.

  4. Show the states

    Draw Idle, MovingUp, MovingDown and DoorsOpen with their transitions. Point out that only DoorsOpen can close the doors.

  5. Add the dispatcher

    Explain the cost rule for choosing a car, and say it sits behind a strategy interface so it can change later.

  6. Walk through edge cases

    Cover the turning point, a blocked door, presses from many threads and a car leaving service. Say which class handles each one.

If time runs short, keep the scheduling rule and the states, and cut the edge cases down to two. Those two ideas carry most of this question.

Questions people ask

Which algorithm do real elevators use?
Modern controllers use more complex heuristic rules, tuned rules of thumb, than the textbook version, according to the Wikipedia article on elevators. Textbooks and interviews start from the elevator algorithm, where a car keeps its direction while requests remain ahead. Groups of cars add a dispatcher, time-of-day modes such as up-peak, and sometimes destination dispatch.
What is the difference between SCAN and LOOK?
SCAN always travels to the end of the building before turning round, while LOOK turns round at the last requested floor in its direction. LOOK avoids wasted trips to empty floors. In the worked example, SCAN travelled 32 floors and LOOK travelled 20.
Why use the state pattern instead of an enum and a switch?
An enum and a switch are fine for two or three simple states. Once each state has its own rules for doors, buttons and motion, the same switch repeats in every method. The state pattern keeps each mode's rules in one class, so adding maintenance mode means adding a class.
How does an elevator avoid starvation?
It serves stops in sweeps instead of always picking the nearest floor. With LOOK, a car keeps its direction while anything is left ahead, so no floor waits forever. In our one-car test, nobody waited for more than 40 floors of travel. In a separate test, nearest first never reached a far floor while nearby calls kept arriving.
How do you handle many people pressing buttons at the same time?
Send every press to one thread-safe event queue and let a single control thread update the cars. A plain TreeSet is not synchronized. In our tests, 8 threads writing to one directly often lost data, and some runs hung, while the queue version was correct every time.
Is the elevator question asked in low level or high level design rounds?
It is mostly a low level design question, about classes, states and one scheduling rule. Some interviewers stretch it toward high level design by asking about many buildings or remote monitoring. Ask which version they want before you start drawing.
What design patterns appear in an elevator system?
The state pattern models the car's modes, and the strategy pattern lets you swap the dispatch rule. Some designs add the observer pattern, so displays update whenever a car changes floor. Use a pattern only where it removes a real problem, not to fill the diagram.

Sources and further reading

Practise this with Lala

Practise