SwadhyAI Notes

All Low-Level Design notes

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.

MovingUp MovingDown Boarding Idle

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.

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
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 empty

Responsibility walk

  1. 01

    Elevator

    Floor, current state, pending stops. Delegates tick and calls to state plus scheduler.

  2. 02

    What hallCall and door mean now. Idle cannot close a door while moving.

  3. 03

    Which pending floor is next. SCAN versus nearest stays swappable here.

  4. 04

    Bank of cars. Assigns a hall call by asking each car for a cost.

  5. 05

    Value object: floor plus direction. Not a boolean flag living on the car.

  6. 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.

java
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

  1. 0–8tick one floor. Assert car at 5 becomes car at 6 after one tick.
  2. 8–20SCAN: pending 9 and 3, direction UP, nextStop must return 9.
  3. 20–32Boarding versus Moving. Prove open() is rejected while the car is moving.
  4. 32–40Queue opposite HallRequest until reverse instead of serving it mid-sweep.
  5. 40–45Add Dispatcher cost if a second car or freight car appears as follow-up.

Tests that prove the seams

java
@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());
}

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.