LLD-003 · DESIGN PRACTICE
Elevator Controller
Separate dispatch policy, movement guards and request ownership in a deterministic simulator.
Local Java 21 reference verified: 20 checks. Learner drafts are not executed. Author self-review; independent review pending.
Existing introductory lesson · Version 1
Understand the problem
Design a multi-car elevator simulation. Assign pickups, accept in-car destinations, advance cars one floor at a time, open doors at stops and reassign pickups when a car leaves service. Explain which rules are safety guards and which are replaceable scheduling choices.
Before you start
- Finite-state machines
- Ordered sets and scheduling
- Atomic state transitions
- Optimistic command versions
Scope and explicit decisions
- This is a discrete in-memory simulator, not certified elevator or real-time hardware software. It has no motor commands, evacuation protocol, travel-time physics, persistence or physical sensor validation.
- Floors are integers 0..floors-1. Each car begins at a configured floor with doors closed. One step moves at most one floor; arrival and floor movement are atomic simulator events.
- Pickup requests are directionless in this implemented slice. Up/down hall buttons, occupancy-aware grouping and hard fairness/maximum-wait guarantees are explicit extension exercises, not already supported features.
- Pickup assignment minimizes distance plus current distinct stop count, with car ID as deterministic tie-breaker. This heuristic is not claimed to minimize average wait or travel. A duplicate pending pickup for a floor returns the same car.
- Each car uses a sweep policy: continue toward pending stops in the current direction, then reverse when no stop remains ahead. An IDLE car initially prefers a higher stop if one exists. In-car and pickup stops at one floor are coalesced.
- Movement requires an in-service car, closed doors and no overload. Door close rejects obstruction or overload. An obstruction flag alone does not alter closed-door motion; it is a door-closing sensor in this model.
- step requires the current per-car version. Requests, sensor flags, service changes and door actions may advance that version, so callers must refresh a stale snapshot rather than force a command.
- Disabling a car reassigns its pending pickup requests before committing the change. Its in-car destinations remain parked for resumption. If no replacement exists, disabling with pending pickups fails atomically. No passenger evacuation behavior is implied.
Worked examples
Input: A starts at 0, B at 8. Request pickup 2.
Outcome: A is assigned. Two steps reach floor 2 and open doors; another step is rejected until close.
Assignment preference is separate from the legal movement transition.
Input: A owns pickup 5, then leaves service while B is enabled.
Outcome: Pickup 5 moves to B; A rejects movement. If A were the only enabled car, the disable operation would fail.
Pending intent must not be lost during service changes.
Your design attempt
Each language has a separate draft across all three levels. Saving is device-only and does not sync to your account. Anyone using this browser profile can load it. Loading is always explicit.
Java 21 reference code is provided for local study. Use Run program in the editor to execute your code and test driver. Program runs show output; they do not grade your design.
Run program · Java 21
No unsaved changes · 208/20000
Self-review
Checks apply to this visit. Self-review is not a passed submission or verified mastery.
Try before revealing
Hint 1
Which method is allowed to change floor?
Hint 2
Which state is hard safety policy, and which state only changes service order?
Hint 3
Keep pickup ownership stable through duplicates and service changes.
Requirements and invariants
R1 — A pending pickup has one assigned serviceable owner. requestPickup protects I1.
R2 — A discrete move cannot bypass door, overload or service guards. step protects I2.
R3 — A stale step cannot move the car twice from an old snapshot. step protects I3.
R4 — Out-of-service reassignment is planned completely before mutation. setOutOfService protects I1.
I1: Each pickup floor appears once in the assignment map and references an enabled car.
I2: The floor changes only through step after verifying closed doors, no overload and in-service status.
I3: A successful step consumes one current version; another step with the old version is rejected without movement.
Derive the design
One car with FIFO requests, then separate policy from guards
A single-car FIFO queue is easy to explain and is correct for a simple request contract, but can send the car back and forth between nearby floors. The reference keeps a sorted destination set and sweep direction, then adds a multi-car assignment map. Dispatch scoring and stop choice are explicit heuristics; door and overload guards remain hard preconditions regardless of the selected policy.
Alternative: An event loop with one writer per car can reduce shared locking, but reassignment then becomes a protocol between controller and car owners. Introduce that boundary only when its ordering and stale-command rules are designed. A strategy interface is useful after there are two real policies, not merely to inflate the class diagram.
Scope physical versus simulated behavior
Ask about direction buttons, capacity, door sensors, timing and emergency service. This implemented contract is deliberately directionless and discrete. Naming those exclusions prevents a small teaching engine from being presented as a safe real-world controller.
Model request ownership independently of stops
Pickup requests need one assigned car; internal destinations belong to a car. stops derives their union rather than duplicating every request into a second authoritative set.
Centralize movement and stale-command checks
Only step modifies floor. Version checks and hard guards precede scheduling, so a policy change cannot move the car through an illegal state.
Keep failure transitions reviewable
Service removal first finds replacement owners. That supports a simple all-or-nothing failure test rather than trying to repair several half-written assignments.
Responsibilities and ownership
Controller
Owns: Car registry, pickup assignments and all local transitions
Methods: requestPickup, selectFloor, step, setOutOfService
One monitor protects request reassignment and car state together.
Car
Owns: Floor, direction, flags, destination set and version
Methods: Private state only
Not exposed to clients; state cannot be mutated around guards.
View
Owns: Immutable snapshot and copied stop list
Methods: Record accessors
A snapshot version is precondition evidence, not permission to ignore new events.
choose / stops
Owns: Assignment scoring and derived stop plan
Methods: Pure calculations under the controller owner
Policy proposals do not override movement guards.
Directionless pickups and discrete steps are explicit scope decisions. Dispatch scoring cannot bypass guards.
- Pickup / destination intent
↓ Choose serviceable owner
Controller assignment + version↓ Check door, overload and service flags
Guarded step↓ Move at most one floor; open on stop
Immutable car view
Internal destinations remain parked for service resumption; passenger evacuation is outside this simulator.
- Car owns pending pickups
↓ Find enabled replacements
Plan replacement map↓ Only commit if every assignment has an owner
Commit or reject without change
Data model, schema and relationships
An elevator controller owns movement safety separately from request selection. Requests describe intent; stops are a scheduled projection rather than proof that a passenger was served.
Implementation boundary: Local car state and stop queues implement safety rules. Persistent requests are an optional dispatch/audit design, not a substitute for hardware interlocks.
These are proposed logical schemas with field contracts, not executable database migrations. PK identifies a row, FK references an existing parent, UK is a uniqueness rule. A composite PK identifies the combination. Nullable references are marked with ?. A foreign key protects identity; cross-row limits, timing and state transitions also require the transaction rules below.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Entity definitions and field contracts
controller
One scheduling/safety ownership scope.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Controller identity |
policy_version | text · VALUE | Scheduling policy |
Code mapping: Controller object.
- One authoritative controller epoch per controlled car.
car
Physical car safety state.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Car identity |
controller_id | uuid · FK | Owning controller · controller.id |
floor | integer · VALUE | Last trusted floor |
door | enum · VALUE | Open or closed |
motion | enum · VALUE | Stopped or moving |
Code mapping: Car state.
- Moving requires closed door and healthy safety sensors.
request
Hall or cabin destination intent.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Request identity |
car_id | uuid · FK | Assigned car · car.id |
floor | integer · VALUE | Requested floor |
status | enum · VALUE | Pending or served |
Code mapping: Request input.
- Unassigned hall requests would require nullable car_id and separate assignment rules; reference uses one car.
stop
Scheduled visit projection.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Stop identity |
car_id | uuid · FK | Car route owner · car.id |
floor | integer · VALUE | Pending stop |
Code mapping: Ordered pending-stop collection.
- Unique active (car_id,floor) coalesces requests without losing their intent records.
Relationship cardinalities and their reasons
| Parent → child | Multiplicity | FK / why |
|---|---|---|
| controller → car controls | 1 parent per child; 0..* children per parent | car.controller_id → controller.idA future dispatcher may own several cars; local reference controls one. |
| car → request serves intent | 1 parent per child; 0..* children per parent | request.car_id → car.idOne car has many requests; assignment does not imply completion. |
| car → stop queues stops | 1 parent per child; 0..* children per parent | stop.car_id → car.idStops are derived scheduling state and may combine several requests. |
Transaction boundaries
- Accept request and update pending stop projection under the car owner.
- A trusted arrival updates floor, stops motion and marks served requests before opening doors under safety guards.
Indexes and uniqueness
- Index pending stops(car_id,floor); unique active stop per floor.
- Index request(car_id,status) for recovery/dispatch; do not infer service merely from deleting a stop.
Schema traps and failure cases
- Database state cannot override a real emergency interlock.
- Out-of-service transitions must stop admission/reassign requests using explicit ownership.
Trace the records through one operation
- Two requests for floor 4 coalesce into one pending stop.
- Controller closes doors before motion command.
- Trusted arrival stops movement; requests are served under the same car state transition.
Method contracts
requestPickup(floor) → carId
Validate floor. Return existing assignment for a duplicate pending request; otherwise select a non-disabled car and record one owner.
selectFloor(carId,floor)
Validate car/floor and reject disabled cars. A set coalesces repeated destination selection.
step(carId,expectedVersion) → View
Validate version and guards first. Select next sweep target, move at most one floor and open doors on a pending stop. Served pickup/destination is removed atomically.
closeDoors; setObstructed; setOverloaded
Sensor flags are trusted simulator inputs. Close refuses obstruction/overload; movement separately checks closed-door and overload state.
setOutOfService(carId,value)
Plan pickup replacement owners before disabling. Reject without state changes if any pending pickup lacks an enabled replacement; preserve internal destinations for later resumption.
What, when, why and how
Each concept is attached to the requirement it protects, its implementation, an alternative and a test.
Safety guard versus scheduling strategy
- what
- Separate non-negotiable movement preconditions from a preference for serving requests.
- when
- When changing a dispatch or sweep algorithm.
- why
- A faster heuristic must never accidentally authorize motion with doors open.
- how
- step checks flags before target selection. choose only assigns pickups; it has no authority to bypass step guards.
Code: step, choose, closeDoors · Invariants: I2
Alternatives, cost and failure example
Alternative: A hardware system needs a separately verified safety controller and sensor protocol; this simulator cannot certify it.
Cost: More explicit layers add interfaces in production, but keep scheduling changes from silently changing safety semantics.
Counterexample: Nearest-car logic directly changes floor while the door-open flag remains true.
Test: Attempt movement at an arrival stop before closing doors; verify rejection and unchanged floor.
Ordered stop set and sweep policy
- what
- Coalesce floor requests and continue in one direction before reversing.
- when
- Several pending floors cause FIFO thrashing.
- why
- Serving nearby stops along the route can reduce unnecessary reversals.
- how
- TreeSet higher/lower selects the next target; current-floor work opens immediately. Direction switches only when no candidate remains ahead.
Code: stops, step · Invariants: I1, I2
Alternatives, cost and failure example
Alternative: FCFS is simpler and can have clearer arrival fairness. Sweep can starve requests under continuous arrivals; aging is a separate enhancement.
Cost: Computing a car stop set currently scans pickups and copies destinations; this is educational simplicity, not a fully indexed hot path.
Counterexample: Treat duplicate floor presses as separate mandatory trips after every completion.
Test: Request floors above and below the current floor and confirm direction persists until reversal is necessary.
Versioned commands and a single local owner
- what
- A command applies only to the state version its caller observed.
- when
- UI, request and sensor events race with movement.
- why
- An old decision may no longer satisfy the current plan or guard state.
- how
- The synchronized step compares expectedVersion before mutation; all other changes use the same monitor and update versions.
Code: step, snapshot, setOverloaded · Invariants: I3
Alternatives, cost and failure example
Alternative: Per-car event sequence numbers are an alternative, but must reject delayed/reordered hardware messages explicitly.
Cost: A coarse controller lock serializes cars, and frequent sensor updates may cause client retries.
Counterexample: Two workers both move a car using the same snapshot version.
Test: Release two step calls simultaneously; one moves to floor one and the other is stale.
Plan before committing reassignment
- what
- Compute all replacements before changing request ownership.
- when
- A car becomes unavailable with pending pickups.
- why
- Disabling first and discovering no replacement later can strand requests.
- how
- setOutOfService builds a replacement map with the disabled candidate excluded; commit assignments and flags only after every choice succeeds.
Code: setOutOfService · Invariants: I1
Alternatives, cost and failure example
Alternative: A larger asynchronous design needs explicit reassignment commands and acknowledgement ownership, possibly with fencing.
Cost: The current scoring does not update temporary load after every proposed assignment; it is deterministic but not globally optimal.
Counterexample: Disable the sole car while it owns a pickup, leaving a map entry referencing a disabled owner.
Test: Fail disabling a sole owner and verify service flag and pickup assignment remain unchanged.
Read and run the Java reference
Reveal complete Solution.java
Java 21. Save as Solution.java; compile with javac --release 21. The repository also contains Test.java and the lld:java verification command. This reference implements the local contract above.
stops derives union rather than storing duplicate ownership
A pickup is authoritative in pickups, while internal destinations are authoritative per car. A fresh sorted union coalesces shared floors.
step has a single floor mutation path
Guards run before a target exists. The floor increments or decrements once, then arrival removes served work and opens doors.
setOutOfService uses a temporary replacement map
All replacement choices succeed before c.disabled changes. This avoids a partial reassignment on a routine no-car error.
Complexity and limits
- With C cars, P pickup requests and D internal destinations per car, choose recomputes stop sets for candidates. A conservative bound is O(C×(P+D log D)); snapshot/step also derive a stop set.
- TreeSet neighbor queries are O(log S) for S distinct stops after constructing that set. Do not label the whole step O(log S) while ignoring derivation work.
- State uses O(C+Dtotal+P); served requests are removed. Version and direction snapshots use O(S) copied stop values.
- One monitor serializes all cars. Indexed per-car pickup sets or event loops require new invariant/ownership tests before performance claims.
Walk through the actual reference
These walkthrough fixtures are emitted only after the Java harness verifies their operations and expected state. Concurrency tests additionally exercise competing callers.
Pickup and door safety
Step 1 of 3: Request pickup at floor 2
Car A assigned from floor 0
Invariant: One owner per pending pickup
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| Request pickup at floor 2 | Car A assigned from floor 0 | One owner per pending pickup |
| Two discrete steps | A at floor 2, doors open | Movement required closed doors |
| Step again before closing | Rejected without moving | Open doors prohibit movement |
Out-of-service reassignment
Step 1 of 2: A owns pickup 5
B remains available
Invariant: Assignment references a serviceable car
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| A owns pickup 5 | B remains available | Assignment references a serviceable car |
| Disable A | Pickup 5 reassigned to B | Plan all replacements before commit |
Mistakes and counterexamples
Let a dispatch policy mutate floor directly
Failing case: An optimized route moves with doors open.
Repair: Policy proposes; a guarded transition owns movement.
Claim sweep guarantees bounded waiting
Failing case: New requests continuously arrive ahead of the car.
Repair: Define load assumptions and add aging/admission rules before giving wait bounds.
Discard pending requests on service removal
Failing case: A customer pickup disappears with the disabled car.
Repair: Reassign ownership atomically or reject the service change.
Ignore a stale snapshot version
Failing case: An overload event arrived after a movement decision.
Repair: Validate current state/version at the authoritative transition.
Engineer → Senior → Staff
Build a deterministic car lifecycle.
Executed: discrete travel, stop coalescing, door guards and overload rejection.
Change exercise
Add tests for current-floor pickup and invalid floors without changing the movement owner.
Worked answer guidance
A current-floor request should open doors without leaving the floor. Invalid inputs must fail before adding pending work.
Interview follow-ups
Why is Direction not the whole movement state?
Door, overload and service flags independently constrain movement. Direction alone cannot express safe action eligibility.
Why use a set for destinations?
Repeated floor selection describes the same pending stop; a queue of duplicates can create redundant service after arrival.
Provenance and scope of evidence
Original teaching material. The local contract is implemented by Solution.java and tested by Test.java on Java 21. Distributed persistence, real provider/device adapters and deployment are not claimed.
Reference verification is not independent editorial review, production deployment verification or a claim that distributed adapters are implemented.