Learning pathsPricingChecking sign-in…
← LLD problem library

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.

Solution.java
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.

Policy and movement ownership

Directionless pickups and discrete steps are explicit scope decisions. Dispatch scoring cannot bypass guards.

  1. Pickup / destination intent
  2. ↓ Choose serviceable owner

    Controller assignment + version
  3. ↓ Check door, overload and service flags

    Guarded step
  4. ↓ Move at most one floor; open on stop

    Immutable car view
Reassignment on service removal

Internal destinations remain parked for service resumption; passenger evacuation is outside this simulator.

  1. Car owns pending pickups
  2. ↓ Find enabled replacements

    Plan replacement map
  3. ↓ 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.

controller: relationship map

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.

controller relationshipscontroller 1 to car 0..*: controls. car.controller_id → controller.id.controls1 parent · 0..* childrencarid [PK]controller_id [FK]floorcontrollerid [PK]policy_version
car: relationship map

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.

car relationshipscar 1 to request 0..*: serves intent. request.car_id → car.id. car 1 to stop 0..*: queues stops. stop.car_id → car.id.serves intent1 parent · 0..* childrenrequestid [PK]car_id [FK]floorqueues stops1 parent · 0..* childrenstopid [PK]car_id [FK]floorcarid [PK]controller_id [FK]floor

Entity definitions and field contracts

controller

One scheduling/safety ownership scope.

FieldType / keyMeaning and reference
iduuid · PKController identity
policy_versiontext · VALUEScheduling policy

Code mapping: Controller object.

  • One authoritative controller epoch per controlled car.

car

Physical car safety state.

FieldType / keyMeaning and reference
iduuid · PKCar identity
controller_iduuid · FKOwning controller · controller.id
floorinteger · VALUELast trusted floor
doorenum · VALUEOpen or closed
motionenum · VALUEStopped or moving

Code mapping: Car state.

  • Moving requires closed door and healthy safety sensors.

request

Hall or cabin destination intent.

FieldType / keyMeaning and reference
iduuid · PKRequest identity
car_iduuid · FKAssigned car · car.id
floorinteger · VALUERequested floor
statusenum · VALUEPending 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.

FieldType / keyMeaning and reference
iduuid · PKStop identity
car_iduuid · FKCar route owner · car.id
floorinteger · VALUEPending 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 → childMultiplicityFK / why
controller → car

controls

1 parent per child; 0..* children per parentcar.controller_id → controller.id

A future dispatcher may own several cars; local reference controls one.

car → request

serves intent

1 parent per child; 0..* children per parentrequest.car_id → car.id

One car has many requests; assignment does not imply completion.

car → stop

queues stops

1 parent per child; 0..* children per parentstop.car_id → car.id

Stops are derived scheduling state and may combine several requests.

Transaction boundaries

  1. Accept request and update pending stop projection under the car owner.
  2. A trusted arrival updates floor, stops motion and marks served requests before opening doors under safety guards.

Indexes and uniqueness

  1. Index pending stops(car_id,floor); unique active stop per floor.
  2. Index request(car_id,status) for recovery/dispatch; do not infer service merely from deleting a stop.

Schema traps and failure cases

  1. Database state cannot override a real emergency interlock.
  2. Out-of-service transitions must stop admission/reassign requests using explicit ownership.

Trace the records through one operation

  1. Two requests for floor 4 coalesce into one pending stop.
  2. Controller closes doors before motion command.
  3. 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.

Solution.java Java 21
import java.util.*;
/** Discrete simulator, not a hardware safety controller. Pickup requests are directionless. */
public final class Solution {
    private Solution() {}
    public enum Direction { IDLE, UP, DOWN }
    public record View(String id,int floor,Direction direction,boolean doorsOpen,boolean overloaded,
                       boolean obstructed,boolean outOfService,long version,List<Integer> stops) {
        public View { stops=List.copyOf(stops); }
    }
    private static final class Car {
        final String id;
        int floor;
        Direction direction=Direction.IDLE;
        boolean doors,overloaded,obstructed,disabled;
        long version;
        final TreeSet<Integer> destinations=new TreeSet<>();
        Car(String id,int floor){this.id=id;this.floor=floor;}
    }
    public static final class Controller {
        private final int floors;
        private final Map<String,Car> cars=new TreeMap<>();
        private final Map<Integer,String> pickups=new TreeMap<>();
        public Controller(int floors,Map<String,Integer> initial){
            if(floors<2||initial.isEmpty())throw new IllegalArgumentException("configuration");this.floors=floors;
            initial.forEach((id,f)->{if(id==null||id.isBlank()||f<0||f>=floors)throw new IllegalArgumentException("car");cars.put(id,new Car(id,f));});
        }
        private void floor(int f){if(f<0||f>=floors)throw new IllegalArgumentException("floor");}
        private Car car(String id){var c=cars.get(id);if(c==null)throw new IllegalArgumentException("car");return c;}
        private TreeSet<Integer> stops(Car c){var result=new TreeSet<>(c.destinations);pickups.forEach((f,id)->{if(id.equals(c.id))result.add(f);});return result;}
        private Car choose(int floor,String excluding){
            return cars.values().stream().filter(c->!c.disabled&&!c.id.equals(excluding)).min(Comparator.comparingLong((Car c)->Math.abs((long)c.floor-floor)+stops(c).size()).thenComparing(c->c.id)).orElseThrow(()->new IllegalStateException("no available car"));
        }
        public synchronized String requestPickup(int floor){
            floor(floor);if(pickups.containsKey(floor))return pickups.get(floor);
            var c=choose(floor,"");pickups.put(floor,c.id);c.version++;return c.id;
        }
        public synchronized void selectFloor(String id,int floor){floor(floor);var c=car(id);if(c.disabled)throw new IllegalStateException("out of service");if(c.destinations.add(floor))c.version++;}
        public synchronized View step(String id,long expectedVersion){
            var c=car(id);if(c.version!=expectedVersion)throw new IllegalStateException("stale command");
            if(c.disabled||c.doors||c.overloaded)throw new IllegalStateException("movement blocked");
            var pending=stops(c);if(pending.isEmpty()){c.direction=Direction.IDLE;c.version++;return view(c);}
            Integer target;
            if(pending.contains(c.floor))target=c.floor;
            else if(c.direction==Direction.DOWN){target=pending.lower(c.floor);if(target==null)target=pending.higher(c.floor);}
            else {target=pending.higher(c.floor);if(target==null)target=pending.lower(c.floor);}
            if(target>c.floor){c.direction=Direction.UP;c.floor++;}
            else if(target<c.floor){c.direction=Direction.DOWN;c.floor--;}
            if(pending.contains(c.floor)){
                c.destinations.remove(c.floor);pickups.remove(c.floor,c.id);c.doors=true;
                if(stops(c).isEmpty())c.direction=Direction.IDLE;
            }
            c.version++;return view(c);
        }
        public synchronized void closeDoors(String id){var c=car(id);if(c.obstructed||c.overloaded)throw new IllegalStateException("door blocked");if(c.doors){c.doors=false;c.version++;}}
        public synchronized void setObstructed(String id,boolean value){var c=car(id);c.obstructed=value;c.version++;}
        public synchronized void setOverloaded(String id,boolean value){var c=car(id);c.overloaded=value;c.version++;}
        public synchronized void setOutOfService(String id,boolean value){
            var c=car(id);if(c.disabled==value)return;
            if(value){
                // Plan every reassignment before committing, so failure leaves all assignments intact.
                var replacement=new TreeMap<Integer,String>();
                pickups.forEach((f,owner)->{if(owner.equals(id))replacement.put(f,choose(f,id).id);});
                c.disabled=true;c.direction=Direction.IDLE;
                replacement.forEach((f,owner)->{pickups.put(f,owner);cars.get(owner).version++;});
            }else c.disabled=false;
            c.version++;
        }
        public synchronized View snapshot(String id){return view(car(id));}
        public synchronized String assignedCar(int floor){floor(floor);return pickups.get(floor);}
        private View view(Car c){return new View(c.id,c.floor,c.direction,c.doors,c.overloaded,c.obstructed,c.disabled,c.version,new ArrayList<>(stops(c)));}
    }
}

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
ActionState after actionInvariant
Request pickup at floor 2Car A assigned from floor 0One owner per pending pickup
Two discrete stepsA at floor 2, doors openMovement required closed doors
Step again before closingRejected without movingOpen 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
ActionState after actionInvariant
A owns pickup 5B remains availableAssignment references a serviceable car
Disable APickup 5 reassigned to BPlan 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

Choose a deep dive

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.

Return to the lesson →

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.