Part 1 of a full machine-coding walkthrough: scoping an elevator system, identifying core classes, and modeling the elevator's behavior as an explicit state machine.
Published September 23, 2026
Nouns: ElevatorSystem, Elevator, Request (internal/external), Dispatcher/Controller. Verbs: requestElevator, pressFloorButton, move, openDoor, closeDoor, dispatch.
An elevator's behavior at any moment is entirely determined by its current state — this is a direct application of State Pattern, and modeling it explicitly (rather than a tangle of booleans like isMoving, isDoorOpen, direction) is what separates a clean design from a fragile one.
IDLE ──(request received)──▶ MOVING_UP / MOVING_DOWN
MOVING_UP/DOWN ──(reached a requested floor)──▶ DOOR_OPEN
DOOR_OPEN ──(timeout / button)──▶ IDLE (no more requests) or MOVING_UP/DOWN (more requests queued)
enum Direction { UP, DOWN, IDLE }
interface ElevatorState {
void handle(Elevator elevator);
}
class IdleState implements ElevatorState {
public void handle(Elevator elevator) {
if (elevator.hasPendingRequests()) {
elevator.setState(elevator.nextTargetIsAbove() ? new MovingUpState() : new MovingDownState());
}
}
}
class MovingUpState implements ElevatorState {
public void handle(Elevator elevator) {
elevator.moveOneFloorUp();
if (elevator.hasRequestAtCurrentFloor()) elevator.setState(new DoorOpenState());
}
}
class MovingDownState implements ElevatorState {
public void handle(Elevator elevator) {
elevator.moveOneFloorDown();
if (elevator.hasRequestAtCurrentFloor()) elevator.setState(new DoorOpenState());
}
}
class DoorOpenState implements ElevatorState {
public void handle(Elevator elevator) {
elevator.clearRequestAtCurrentFloor();
elevator.setState(new IdleState()); // IdleState.handle() immediately re-evaluates for remaining requests
}
}
class Elevator {
private final int id;
private int currentFloor = 0;
private ElevatorState state = new IdleState();
private final TreeSet<Integer> upRequests = new TreeSet<>(); // sorted — nearest requests first
private final TreeSet<Integer> downRequests = new TreeSet<>(Comparator.reverseOrder());
void setState(ElevatorState state) { this.state = state; }
void step() { state.handle(this); } // called repeatedly, e.g. by a scheduler tick
void addInternalRequest(int floor) {
if (floor > currentFloor) upRequests.add(floor); else if (floor < currentFloor) downRequests.add(floor);
}
boolean hasPendingRequests() { return !upRequests.isEmpty() || !downRequests.isEmpty(); }
boolean nextTargetIsAbove() { return !upRequests.isEmpty(); }
void moveOneFloorUp() { currentFloor++; }
void moveOneFloorDown() { currentFloor--; }
boolean hasRequestAtCurrentFloor() { return upRequests.contains(currentFloor) || downRequests.contains(currentFloor); }
void clearRequestAtCurrentFloor() { upRequests.remove(currentFloor); downRequests.remove(currentFloor); }
}
Per State Pattern's own guidance on when the pattern earns its complexity: an elevator's states have meaningfully different behavior (moving up computes a different next action than door-open does), not just a different label — and the transition logic itself varies enough per state (idle re-evaluates whether to move at all; door-open unconditionally returns to idle) that a single switch on an enum would accumulate real branching complexity. This is close to the strongest possible case for full State-pattern classes, not the traffic-light-simple case where an enum+switch would suffice.
class ElevatorSystem {
private final List<Elevator> elevators;
ElevatorSystem(List<Elevator> elevators) { this.elevators = elevators; }
void requestElevator(int floor, Direction direction) {
// Part 2: which elevator answers this call? Left unimplemented here deliberately.
}
}
This lesson deliberately leaves requestElevator's dispatch logic as a stub — Elevator System — Implementation & Scheduling is entirely about that decision: which of several elevators should answer an external call, and how in-progress requests merge with new ones.
Q: Why TreeSet for pending requests instead of a plain List?
A: A sorted set keeps the nearest pending floor immediately accessible (first()/last()) without a linear scan or a separate sort step every time a new request is added — upRequests sorted ascending naturally gives 'nearest floor above, going up' as the next stop, and downRequests sorted descending gives the same for going down.
Q: What happens if a new internal request arrives for a floor the elevator already passed while moving up? A: With the split-by-direction TreeSet design, a floor below the current position moving up would go into downRequests, not upRequests — meaning the elevator would need to finish its current up-direction sweep before addressing it. This mirrors how real elevators behave (see the SCAN algorithm in Part 2) rather than immediately reversing direction for every new request, which would be inefficient and disorienting for other passengers already inside.
Q: Is DoorOpenState really necessary as its own state, or could 'stopping at a floor' just be an action inside MovingUpState/MovingDownState? A: Making it explicit is what allows door-related concerns (a timeout before auto-closing, an obstruction sensor keeping it open, a manual close button) to be modeled cleanly later without those concerns bleeding into the movement states — exactly the kind of 'meaningfully different behavior per state' that justifies State pattern's structure over a simpler boolean flag.