State + scheduling case study
Elevator system
Imagine a car on floor 5 going up with a passenger to 9, while floor 3 presses Up and floor 8 presses Down. If you pick the nearest button every , the car dives to 3, dumps the passenger to 9, and yo-yos. That is the whole problem this page solves.
An elevator is a car plus a . Direction, pending stops, and door rules live in the current state. collects in-direction stops first, then reverses. Nearest-car in a bank is another strategy on a , not a rewrite of Elevator.move().
Snapshot of the shaft
- F9·
- F8↓
- F7·
- F6·
- F5■ car
- F4·
- F3↑
- F2·
- F1·
SCAN-like: collect 9 (car) then maybe 8 Down after reverse, not 3 Up first just because it is nearer.
What actually happens after the first tick
The shaft snapshot above is the default interview story. Direction is not a boolean; it is part of . Idle, MovingUp, MovingDown, and Boarding each answer hallCall, carCall, and differently. A is a floor plus a desired direction. A CarRequest is a floor the passenger pressed inside. The car collects stops that make sense in the current direction, then reverses.
Time is discrete in machine coding: tick moves one floor, or a scheduler thread advances an injected Clock. Do not sleep in the domain. Tests should say "car at 5, request at 7, one tick, car at 6." A bank of cars is a that asks each car for a cost, then assigns. Cost is a strategy. Put door rules in the current state object, not in a 40-line if. See state machines.
Simple rule of thumb
Before you draw classes, you need a fast way to reject the design that looks like an elevator but yo-yos. Direction first, nearest later: a car at 5 going up to 9 must not dive to 3 because 3 is nearer.
collects the current sweep. forbids open-door-while-moving. moves one floor. Clock or test-driven tick; no sleep in the domain. A bank is a with a cost strategy; freight ignores passenger hall calls without rewriting Elevator.tick.
How to explain this in an interview
Say: state forbids illegal doors, strategy picks nextStop, tick is one floor, dispatcher cost owns the bank. Nearest is a strategy you choose out loud, not default physics.
Legal arrows and who owns what
The rule of thumb names the seams; this is the legal state graph and the responsibility walk. Only the arrows below are legal. Door open is Boarding only.
Read the diagram definition
stateDiagram-v2
[*] --> Idle
Idle --> MovingUp: hall Up or car floor above
Idle --> MovingDown: hall Down or car floor below
MovingUp --> Boarding: next stop in this sweep
MovingDown --> Boarding: next stop in this sweep
Boarding --> MovingUp: more stops above
Boarding --> MovingDown: more stops below
Boarding --> Idle: pending empty
MovingUp --> Idle: pending emptyResponsibility walk
- 01
Elevator
Floor, current state, pending stops. Delegates tick and calls to state plus scheduler.
- 02
What hallCall and door mean now. Idle cannot close a door while moving.
- 03
Which pending floor is next. SCAN versus nearest stays swappable here.
- 04
Bank of cars. Assigns a hall call by asking each car for a cost.
- 05
Value object: floor plus direction. Not a boolean flag living on the car.
- 06
Tests drive motion. Domain never Thread.sleep(1000) once per floor.
Java API sketch
The diagram above is the same object graph the sketch implements: state owns verbs, strategy owns nextStop, dispatcher owns the bank.
enum Direction { UP, DOWN, IDLE }
interface ElevatorState {
void hallCall(Elevator car, HallRequest request);
void carCall(Elevator car, int floor);
void tick(Elevator car);
}
interface SchedulingStrategy {
Optional<Integer> nextStop(ElevatorSnapshot car, Set<Integer> pending);
}
interface Dispatcher {
void assign(HallRequest request, List<Elevator> bank);
}
class Elevator {
private ElevatorState state;
private final SchedulingStrategy scheduler;
private int floor;
void tick() { state.tick(this); }
}Worked sweep: floors 5, 9, 3, and 8
Walk the shaft from the hero with car at 5, MovingUp, car call 9, hall Up at 3, hall Down at 8. Pending in this direction: 9. Hall Down at 8 is the opposite hall, so says serve 9 first, then reverse, then 8 Down, then 3 Up. Nearest-every-tick would go 5 → 3 because 3 is closer, dumping the passenger to 9. That is the yo-yo named in the header.
Numbers to know
- Car at 5 with call to 9: nextStop must be 9, not nearer hall Up at 3.
- One floor per tick (or explicit travel time) so tests assert 5 then 6.
- Door open is Boarding only; open() in MovingUp must fail in a test.
- Freight returns infinite cost for passenger hall calls at the dispatcher.
Minute 0–10
Draw states and forbidden arrows. tick moves one floor. Inject Clock or a test-driven tick.
Minute 10–25
SchedulingStrategy.nextStop. Hard-code SCAN. Tests: pending 9 and 3, direction UP, next is 9.
If a bank appears
Dispatcher asks each car for a cost. Freight car returns infinite cost for passenger hall calls.
That open()-while-moving failure is how you prove State is doing work, not a status enum you ignore. See the State lab.
Invalid cases, and what still breaks
The worked sweep fails in interviews for a short list of mistakes. Each one either breaks , ignores , or races pending stops across threads.
Invalid cases
- Open the door while MovingUp: door rules live in Boarding; Moving states reject open().
- Reverse from 5 to 3 while a passenger to 9 is aboard under SCAN: collect current-direction stops first.
- Hall Down accepted as next stop during a committed Up sweep: queue it until reverse.
- tick() that jumps from 1 to 9: one floor per tick so tests need stepwise motion.
- Freight car serving passenger hall calls: filter in Dispatcher cost, not Elevator.tick().
Concurrency
Hall buttons and may run on different threads if you simulate hardware. Pending stops need a concurrent set or a lock around "add request + choose next." (one dispatcher thread that owns the bank) is an honest first answer rather than synchronizing every floor independently.
Still fails when…
- Nearest-every-tick yo-yos and drops the passenger request already going to floor 9.
- open() while moving succeeds because door rules were not in Boarding state.
- Two hardware threads mutate pending without a lock or one owner thread.
Trade-offs
- SCAN is fairer for passengers aboard; nearest looks simpler and yo-yos under load.
- One owner thread is honest; fine-grained floor locks are a later throughput follow-up.
- A working SCAN list beats an empty strategy interface that never picks nextStop.
What the follow-up changes
Once state and scheduling in the failure card are honest, interviewers stretch the same object. Do not turn Elevator.move() into a policy encyclopedia.
- Second car or freight car. Land as cost or a . Elevator.move() stays a tick of physics.
- Express to 20+ only. Filter in dispatcher cost or car type, not a new if inside every state's hallCall.
- State pattern lab. State pattern lab deepens the same door-and-transition seams without rebuilding the shaft.
Wrap-up
You have the seams, the worked sweep, and the invalid cases. Use the forty-five-minute plan, then the seam test, then the related notes.
Forty-five minutes
- 0–8tick one floor. Assert car at 5 becomes car at 6 after one tick.
- 8–20SCAN: pending 9 and 3, direction UP, nextStop must return 9.
- 20–32Boarding versus Moving. Prove open() is rejected while the car is moving.
- 32–40Queue opposite HallRequest until reverse instead of serving it mid-sweep.
- 40–45Add Dispatcher cost if a second car or freight car appears as follow-up.
Tests that prove the seams
@Test
void sweepCollectsNineBeforeThree() {
Elevator car = Elevator.at(5, Direction.UP);
car.carCall(9);
car.hallCall(new HallRequest(3, Direction.UP));
assertEquals(9, car.scheduler().nextStop(car.snapshot(), car.pending()).orElseThrow());
}Read next
Key takeaway
You're done when hall and cabin requests are scheduled by a strategy, the car state machine rejects impossible moves, and two elevators (if modeled) do not both claim the same request without a clear assignment rule. This is not nearest-every-tick; collects the current direction, then reverses.