Knowledge Guide
HomeHands-On BuildsLLD Katas

Design an Elevator System

Design an Elevator System

You already know the ingredients in isolation: a state machine from the State Pattern page, and a pluggable algorithm from the Strategy Pattern walkthrough. "Design an Elevator System" is the interview question that forces you to wire those two ideas into one working system: a car that is IDLE/UP/DOWN, a scheduler that decides which floor it visits next, and — the moment the interviewer asks "what about 10 cars in one building" — a dispatcher that decides which car answers which call. This project makes you build that in Go and Java, from an empty file, and prove why naive first-come-first-served scheduling is the wrong answer before you even get to the concurrency bug hiding underneath it.

The Trap

Model an elevator the most literal way possible: a queue of requested floors, serviced in the order they arrived.

// The naive approach — looks reasonable, ships broken.
Queue<Integer> requests = new LinkedList<>();
while (true) {
    int destination = requests.poll();   // whichever floor was requested first
    moveTo(destination);                  // travel there directly, however far
}

Ride it as a passenger. The car sits at floor 0. Floor 5 requests it, then floor 1, then floor 9, then floor 2, then floor 8. FCFS honors that literal arrival order: 0 → 5 → 1 → 9 → 2 → 8. You just watched the car climb to 5, drop nearly to the ground, rocket to the top, crash back down near the bottom, then climb again — for five requests that a sane building services in one pass from bottom to top. Worse than the wasted travel: if a sixth request keeps re-arriving near the bottom every time the car passes by, the car keeps servicing that neighborhood while a request sitting at floor 9 waits arbitrarily far back in the queue. That's starvation — a request's wait time depends on queue position, not on how close the car already is to it. The naive model isn't just slow, it's incoherent: it ignores the one fact that should drive every decision — where the car physically is right now, and which way it's already moving.

Scope it like a senior

Pin down the contract before writing a scheduler — these are the questions that separate "I can draw the classes" from "I understand what a real elevator bank promises":

Answer for this kata: N cars (configurable, default 1 for M1–M3), hall calls (floor + direction) and car calls (floor only) both modeled as the same underlying "stop," a per-car capacity limit, LOOK scheduling, and emergency/maintenance flagged as a named extension rather than built.

Reason to the design

The state you actually need: a car has a DirectionUP, DOWN, or IDLE — and that direction is exactly what should decide which request gets served next. It's the State Pattern in its simplest honest form: three states, clear transitions (IDLE→UP/DOWN when a stop is added; UP/DOWN→IDLE when both stop sets empty; UP↔DOWN when the current sweep runs dry but the other direction still has work).

Simplest thing that could work: one shared queue of destination floors, FCFS — exactly the trap above.

Why it fails: already shown. It ignores the car's current position and direction entirely, so it can't tell "this request is 2 floors away, on my way" from "this request is on the opposite side of the building." Any scheduler that doesn't use position + direction as its primary signal will ping-pong.

The fix — sort by direction, not by arrival time: keep two sorted collections per car — upStops and downStops — instead of one FIFO queue. When the car is heading UP, it always serves its nearest unserved floor above next, not whichever request happened to arrive first. This is the disk-scheduling analogy made concrete: SCAN ("the elevator algorithm" in OS textbooks) sweeps from one physical end to the other, servicing every pending request, reversing only at the edge. LOOK is the practical refinement — it reverses the instant there's nothing left requested ahead of it, without traveling all the way to the top floor if the highest requested floor is 9 out of a 20-floor building. LOOK is strictly no worse than SCAN, and strictly better whenever requests don't span the full range — in real buildings, that's basically always. This kata builds LOOK.

Hall calls vs. car calls, mechanically: a car call (rider already inside, pressed "12") always becomes a stop in whichever set matches its position relative to the car — the rider is already committed to this car, no judgment needed. A hall call (rider on floor 7 pressed ▼) is different in kind: it should ideally only land on a car that is already heading that direction and hasn't passed that floor yet — that decision belongs to the Dispatcher (M4), which scores every candidate car via etaTo(floor, direction) rather than blindly handing the stop to whichever car happens to be nearest in raw distance.

Build it — milestones

Reference implementation — Java: the Car (LOOK scheduling)

Two sorted stop sets ARE the scheduler — there's no separate "algorithm" object. tick() reverses the moment its stop set empties, which is what makes this LOOK instead of SCAN: notice there's no topFloor/bottomFloor constant anywhere. That absence is the optimization.

import java.util.NavigableSet;
import java.util.TreeSet;
import java.util.concurrent.locks.ReentrantLock;

/** One elevator car running LOOK scheduling: two sorted stop sets (up / down),
 *  keep moving in the current direction servicing stops, reverse only once
 *  there are no more stops AHEAD (not at the building's top/bottom -- that's
 *  what makes this LOOK instead of SCAN). */
final class Car {
    enum Direction { UP, DOWN, IDLE }

    final int id;
    private final int capacity;
    private int currentFloor;
    private Direction direction = Direction.IDLE;
    private int occupancy = 0;

    private final NavigableSet<Integer> upStops = new TreeSet<>();    // ascending: farthest-up = last()
    private final NavigableSet<Integer> downStops = new TreeSet<>();  // ascending: farthest-down = first()
    private final ReentrantLock lock = new ReentrantLock();

    Car(int id, int capacity, int startFloor) {
        this.id = id;
        this.capacity = capacity;
        this.currentFloor = startFloor;
    }

    int currentFloor() { lock.lock(); try { return currentFloor; } finally { lock.unlock(); } }
    Direction direction() { lock.lock(); try { return direction; } finally { lock.unlock(); } }

    /** A rider inside the car presses a floor button, OR the Dispatcher assigns a hall call to this car. */
    void addStop(int floor) {
        lock.lock();
        try {
            if (floor > currentFloor) upStops.add(floor);
            else if (floor < currentFloor) downStops.add(floor);
            if (direction == Direction.IDLE && floor != currentFloor) {
                direction = floor > currentFloor ? Direction.UP : Direction.DOWN;
            }
        } finally {
            lock.unlock();
        }
    }

    /** LOOK-aware cost estimate: floors this car must travel before it could pick up
     *  a hall call at `floor` heading `dir`. "On the way" is cheap; "behind" costs a
     *  full reversal to the current sweep's turnaround point first. */
    int etaTo(int floor, Direction dir) {
        lock.lock();
        try {
            if (direction == Direction.IDLE) return Math.abs(floor - currentFloor);
            if (direction == Direction.UP && dir == Direction.UP && floor >= currentFloor) {
                return floor - currentFloor;
            }
            if (direction == Direction.DOWN && dir == Direction.DOWN && floor <= currentFloor) {
                return currentFloor - floor;
            }
            if (direction == Direction.UP) {
                int turnaround = upStops.isEmpty() ? currentFloor : upStops.last();
                return (turnaround - currentFloor) + Math.abs(turnaround - floor);
            } else {
                int turnaround = downStops.isEmpty() ? currentFloor : downStops.first();
                return (currentFloor - turnaround) + Math.abs(turnaround - floor);
            }
        } finally {
            lock.unlock();
        }
    }

    boolean hasCapacity() { lock.lock(); try { return occupancy < capacity; } finally { lock.unlock(); } }

    /** Capacity check-and-increment as ONE atomic step under the lock -- see the break-it test for why. */
    boolean tryBoard() {
        lock.lock();
        try {
            if (occupancy >= capacity) return false;
            occupancy++;
            return true;
        } finally {
            lock.unlock();
        }
    }

    void exit() { lock.lock(); try { occupancy = Math.max(0, occupancy - 1); } finally { lock.unlock(); } }

    int occupancy() { lock.lock(); try { return occupancy; } finally { lock.unlock(); } }

    /** One LOOK tick: advance exactly one floor toward `direction`, service a stop if
     *  reached, and reverse at the LAST requested stop (not the building's edge --
     *  that's the whole saving LOOK has over SCAN). Returns the serviced floor, or -1. */
    int tick() {
        lock.lock();
        try {
            if (direction == Direction.UP) {
                if (upStops.isEmpty()) {
                    direction = downStops.isEmpty() ? Direction.IDLE : Direction.DOWN;
                    return -1;
                }
                currentFloor++;
                return upStops.remove(currentFloor) ? currentFloor : -1;
            } else if (direction == Direction.DOWN) {
                if (downStops.isEmpty()) {
                    direction = upStops.isEmpty() ? Direction.IDLE : Direction.UP;
                    return -1;
                }
                currentFloor--;
                return downStops.remove(currentFloor) ? currentFloor : -1;
            }
            return -1;
        } finally {
            lock.unlock();
        }
    }
}

Reference implementation — Java: the Dispatcher (thread-safe multi-car assignment)

import java.util.List;

/** Assigns each hall call to the nearest suitable car. The whole
 *  select-then-commit (read every car's ETA, pick the best, addStop on it)
 *  is ONE critical section -- split it into two steps and two hall calls
 *  can race onto the same "best" car's last seat (see the break-it test). */
final class Dispatcher {
    private final List<Car> cars;
    private final Object assignLock = new Object();

    Dispatcher(List<Car> cars) {
        this.cars = cars;
    }

    int assign(int floor, Car.Direction dir) {
        synchronized (assignLock) {
            Car best = null;
            int bestEta = Integer.MAX_VALUE;
            for (Car c : cars) {
                if (!c.hasCapacity()) continue;
                int eta = c.etaTo(floor, dir);
                if (eta < bestEta) {
                    bestEta = eta;
                    best = c;
                }
            }
            if (best == null) throw new IllegalStateException("no car has capacity");
            best.addStop(floor);
            return best.id;
        }
    }
}

Reference implementation — Go: Car + Dispatcher

Same shape as the Java version, one sync.Mutex per car for stop-set mutation and a separate dispatchMu in the Dispatcher guarding only the select-then-commit sequence.

package main

import "sync"

type Direction int

const (
	Idle Direction = iota
	Up
	Down
)

// Car is one elevator car running LOOK scheduling: two stop sets (up/down),
// keep moving in the current direction servicing stops, reverse only once
// there are no more stops AHEAD -- not at the building's top/bottom (that's
// what makes this LOOK instead of SCAN).
type Car struct {
	ID       int
	capacity int

	mu        sync.Mutex
	floor     int
	direction Direction
	occupancy int
	upStops   map[int]bool
	downStops map[int]bool
}

func NewCar(id, capacity, startFloor int) *Car {
	return &Car{
		ID:        id,
		capacity:  capacity,
		floor:     startFloor,
		direction: Idle,
		upStops:   make(map[int]bool),
		downStops: make(map[int]bool),
	}
}

func (c *Car) CurrentFloor() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.floor
}

func (c *Car) DirectionNow() Direction {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.direction
}

// AddStop registers a floor request: a rider's car button, or a hall call the Dispatcher assigned here.
func (c *Car) AddStop(floor int) {
	c.mu.Lock()
	defer c.mu.Unlock()
	switch {
	case floor > c.floor:
		c.upStops[floor] = true
	case floor < c.floor:
		c.downStops[floor] = true
	}
	if c.direction == Idle && floor != c.floor {
		if floor > c.floor {
			c.direction = Up
		} else {
			c.direction = Down
		}
	}
}

func abs(x int) int {
	if x < 0 {
		return -x
	}
	return x
}

func (c *Car) upTurnaround() int {
	max := c.floor
	for f := range c.upStops {
		if f > max {
			max = f
		}
	}
	return max
}

func (c *Car) downTurnaround() int {
	min := c.floor
	first := true
	for f := range c.downStops {
		if first || f < min {
			min = f
			first = false
		}
	}
	return min
}

// EtaTo estimates floors-to-travel before this car could serve a hall call at
// `floor` heading `dir` -- LOOK-aware: "on the way" is cheap, "behind" costs a full reversal first.
func (c *Car) EtaTo(floor int, dir Direction) int {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.direction == Idle {
		return abs(floor - c.floor)
	}
	if c.direction == Up && dir == Up && floor >= c.floor {
		return floor - c.floor
	}
	if c.direction == Down && dir == Down && floor <= c.floor {
		return c.floor - floor
	}
	if c.direction == Up {
		t := c.upTurnaround()
		return (t - c.floor) + abs(t-floor)
	}
	t := c.downTurnaround()
	return (c.floor - t) + abs(t-floor)
}

func (c *Car) HasCapacity() bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.occupancy < c.capacity
}

// TryBoard does the capacity check-and-increment as ONE atomic step under the lock.
func (c *Car) TryBoard() bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.occupancy >= c.capacity {
		return false
	}
	c.occupancy++
	return true
}

func (c *Car) Exit() {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.occupancy > 0 {
		c.occupancy--
	}
}

func (c *Car) Occupancy() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.occupancy
}

// Tick advances exactly one floor toward the current direction, servicing a
// stop if reached, and reverses at the LAST requested stop (not the
// building's edge -- LOOK's saving over SCAN). Returns the serviced floor, or -1.
func (c *Car) Tick() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	switch c.direction {
	case Up:
		if len(c.upStops) == 0 {
			if len(c.downStops) == 0 {
				c.direction = Idle
			} else {
				c.direction = Down
			}
			return -1
		}
		c.floor++
		if c.upStops[c.floor] {
			delete(c.upStops, c.floor)
			return c.floor
		}
		return -1
	case Down:
		if len(c.downStops) == 0 {
			if len(c.upStops) == 0 {
				c.direction = Idle
			} else {
				c.direction = Up
			}
			return -1
		}
		c.floor--
		if c.downStops[c.floor] {
			delete(c.downStops, c.floor)
			return c.floor
		}
		return -1
	default:
		return -1
	}
}

// Dispatcher assigns a hall call to the best (lowest-ETA) car with room. The
// whole select-then-commit is ONE critical section under dispatchMu -- that's
// what prevents two hall calls from racing onto the same car's last seat
// based on two stale "has capacity" reads (see the break-it test).
type Dispatcher struct {
	cars       []*Car
	dispatchMu sync.Mutex
}

func NewDispatcher(cars []*Car) *Dispatcher {
	return &Dispatcher{cars: cars}
}

func (d *Dispatcher) Assign(floor int, dir Direction) (int, bool) {
	d.dispatchMu.Lock()
	defer d.dispatchMu.Unlock()
	bestID := -1
	bestEta := 1 << 30
	var bestCar *Car
	for _, c := range d.cars {
		if !c.HasCapacity() {
			continue
		}
		eta := c.EtaTo(floor, dir)
		if eta < bestEta {
			bestEta = eta
			bestCar = c
			bestID = c.ID
		}
	}
	if bestCar == nil {
		return -1, false
	}
	bestCar.AddStop(floor)
	return bestID, true
}

Break it

Two failures, both real and both measured in this session.

1 — FCFS vs. LOOK, the same request set. Feed the identical requests from The Trap (5, 1, 9, 2, 8, car starting at floor 0) into a plain arrival-order sum and into a real Car running LOOK, and count total floors traveled by each:

2 — the capacity race. tryBoard() on the real Car does "check occupancy, then increment" as one locked step. Split that into two unsynchronized steps (a Thread.yield()/runtime.Gosched() planted deliberately in between to widen the window) and race 200 threads for a single seat:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

/** Same idea as Car.tryBoard(), but the capacity check and the increment are
 *  NOT one atomic step -- two threads can both pass the check before either
 *  increments. That gap is the bug. */
final class UnsafeCar {
    private final int capacity;
    private volatile int occupancy = 0;

    UnsafeCar(int capacity) { this.capacity = capacity; }

    boolean tryBoard() {
        if (occupancy >= capacity) return false;   // CHECK
        Thread.yield();                              // widen the race window so it shows up every run
        occupancy++;                                 // ...then ACT -- another thread can slip in right here
        return true;
    }

    int occupancy() { return occupancy; }
}

final class ElevatorBreakIt {

    public static void main(String[] args) throws InterruptedException {
        // ---- Part 1: FCFS vs LOOK -- same request set, two schedulers, real numbers ----
        int[] requests = {5, 1, 9, 2, 8};   // arrival order; car starts at floor 0

        int fcfsDistance = 0, at = 0;
        for (int r : requests) {
            fcfsDistance += Math.abs(r - at);
            at = r;
        }
        // FCFS services strictly in arrival order: 0->5->1->9->2->8 -- ping-pongs across the shaft.

        Car look = new Car(1, Integer.MAX_VALUE, 0);
        for (int r : requests) look.addStop(r);
        int lookDistance = 0;
        while (look.direction() != Car.Direction.IDLE) {
            int before = look.currentFloor();
            look.tick();
            lookDistance += Math.abs(look.currentFloor() - before);
        }
        // LOOK sorts the same 5 requests into one monotonic upward sweep: 0->1->2->5->8->9.

        System.out.println("FCFS total floors traveled = " + fcfsDistance);
        System.out.println("LOOK total floors traveled = " + lookDistance);
        if (fcfsDistance != 30) throw new AssertionError("expected FCFS=30, got " + fcfsDistance);
        if (lookDistance != 9) throw new AssertionError("expected LOOK=9, got " + lookDistance);
        System.out.println("FCFS travels " + (fcfsDistance - lookDistance) + " extra floors for the identical request set.");

        // ---- Part 2: the capacity race -- 200 threads racing for ONE seat ----
        Car safe = new Car(2, 1, 0);   // capacity = 1
        AtomicInteger safeAccepted = new AtomicInteger();
        race(200, safe::tryBoard, safeAccepted);
        System.out.println("Locked Car:     accepted=" + safeAccepted.get() + " (capacity=1)");
        if (safeAccepted.get() != 1) throw new AssertionError("locked car should accept exactly 1, got " + safeAccepted.get());

        UnsafeCar unsafe = new UnsafeCar(1);
        AtomicInteger unsafeAccepted = new AtomicInteger();
        race(200, unsafe::tryBoard, unsafeAccepted);
        System.out.println("Unlocked UnsafeCar: accepted=" + unsafeAccepted.get() + " (capacity=1) -- THE BUG: more than 1 rider boarded a 1-seat car.");
        if (unsafeAccepted.get() <= 1) {
            System.out.println("(race didn't trigger this run -- rerun; the check-then-act gap is still there)");
        }
    }

    interface BoolCall { boolean call(); }

    private static void race(int n, BoolCall attempt, AtomicInteger accepted) throws InterruptedException {
        CountDownLatch start = new CountDownLatch(1);
        Thread[] threads = new Thread[n];
        for (int i = 0; i < n; i++) {
            threads[i] = new Thread(() -> {
                try { start.await(); } catch (InterruptedException ignored) { }
                if (attempt.call()) accepted.incrementAndGet();
            });
            threads[i].start();
        }
        start.countDown();
        for (Thread t : threads) t.join();
    }
}

Measured, this session (javac -Xlint:all -Werror, run three times): FCFS total floors traveled = 30, LOOK total floors traveled = 9 — every run, deterministic. Locked Car: accepted=1 every run. Unlocked UnsafeCar: accepted=2, then 3, then 3 across three separate runs — more than one rider boarded a car with exactly one seat, every single time. Same shape in Go (below): go vet clean, go build -race clean — and the race detector reports no data race on the unsafe version, because every access to occupancy uses atomic.LoadInt32/AddInt32. That's the sharpest part of the lesson: -race only catches unsynchronized memory access, not a broken check-then-act invariant. The memory is safe; the logic still overbooks the car. Atomics are not a substitute for a lock around a multi-step decision.

package main

import (
	"fmt"
	"runtime"
	"sync"
	"sync/atomic"
)

// UnsafeCar mirrors Car.TryBoard, but the capacity check and the increment
// are NOT one atomic step -- two goroutines can both pass the check before
// either increments. That gap is the bug.
type UnsafeCar struct {
	capacity  int
	occupancy int32
}

func (u *UnsafeCar) TryBoard() bool {
	if int(atomic.LoadInt32(&u.occupancy)) >= u.capacity { // CHECK
		return false
	}
	runtime.Gosched()                // widen the race window so it shows up every run
	atomic.AddInt32(&u.occupancy, 1) // ...then ACT -- another goroutine can slip in right here
	return true
}

func race(n int, attempt func() bool) int32 {
	var accepted int32
	var wg sync.WaitGroup
	start := make(chan struct{})
	wg.Add(n)
	for i := 0; i < n; i++ {
		go func() {
			defer wg.Done()
			<-start
			if attempt() {
				atomic.AddInt32(&accepted, 1)
			}
		}()
	}
	close(start)
	wg.Wait()
	return accepted
}

func main() {
	// ---- Part 1: FCFS vs LOOK -- same request set, two schedulers, real numbers ----
	requests := []int{5, 1, 9, 2, 8} // arrival order; car starts at floor 0

	fcfsDistance, at := 0, 0
	for _, r := range requests {
		fcfsDistance += abs(r - at)
		at = r
	}
	// FCFS services strictly in arrival order: 0->5->1->9->2->8 -- ping-pongs across the shaft.

	look := NewCar(1, 1<<30, 0)
	for _, r := range requests {
		look.AddStop(r)
	}
	lookDistance := 0
	for look.DirectionNow() != Idle {
		before := look.CurrentFloor()
		look.Tick()
		lookDistance += abs(look.CurrentFloor() - before)
	}
	// LOOK sorts the same 5 requests into one monotonic upward sweep: 0->1->2->5->8->9.

	fmt.Println("FCFS total floors traveled =", fcfsDistance)
	fmt.Println("LOOK total floors traveled =", lookDistance)
	if fcfsDistance != 30 {
		panic(fmt.Sprintf("expected FCFS=30, got %d", fcfsDistance))
	}
	if lookDistance != 9 {
		panic(fmt.Sprintf("expected LOOK=9, got %d", lookDistance))
	}
	fmt.Println("FCFS travels", fcfsDistance-lookDistance, "extra floors for the identical request set.")

	// ---- Part 2: the capacity race -- 200 goroutines racing for ONE seat ----
	safe := NewCar(2, 1, 0) // capacity = 1
	safeAccepted := race(200, safe.TryBoard)
	fmt.Println("Locked Car:         accepted =", safeAccepted, "(capacity=1)")
	if safeAccepted != 1 {
		panic(fmt.Sprintf("locked car should accept exactly 1, got %d", safeAccepted))
	}

	unsafeCar := &UnsafeCar{capacity: 1}
	unsafeAccepted := race(200, unsafeCar.TryBoard)
	fmt.Println("Unlocked UnsafeCar: accepted =", unsafeAccepted, "(capacity=1) -- THE BUG: more than 1 rider boarded a 1-seat car.")
}

Tests to write (this is the real learning)

Optimise — trade-offs

DecisionOption AOption BWhen A winsWhen B wins
Scheduling disciplineFCFS (arrival order)LOOK (direction-sorted sweep)Almost never at production scale — only defensible when there is provably never more than one outstanding request at a time, so there's nothing to reorderAny real traffic pattern — always reduces total travel and bounds worst-case wait; this kata's baseline for a reason
Sweep policySCAN (reverse at the building's physical edge)LOOK (reverse at the last requested stop)Marginal implementation simplicity if you already track a fixed floor range and don't want to compute "farthest pending stop" — rarely worth itRequests don't span the full building range — true almost always in practice, so LOOK dominates: it never pays for travel nobody asked for
Next-request choiceLOOK (serve in direction order)SSTF — shortest-seek-time-first (always serve the nearest pending request, regardless of direction)Worst-case wait matters — LOOK guarantees every pending stop is reached within one full sweep, no request waits foreverMinimizing the very next response time matters more than fairness — but SSTF can starve a far request indefinitely if near requests keep arriving, the same starvation trap as FCFS in a different disguise
Locking granularityOne global dispatcher-wide lockPer-car lock + a separate, narrow dispatch-assignment lock (this kata's design)Car count is small (a handful) and assignment is infrequent relative to car activity — contention is negligible, and one lock is simpler to reason aboutMany cars, or frequent ticking/boarding — a global lock would serialize unrelated cars' internal state changes against every dispatch decision; per-car locks let Car A's tick() run while Car B is being scored
Dispatch policyFixed nearest-ETA (this kata)Pluggable Strategy (zone-based, load-balanced, destination-dispatch)Small buildings, simple traffic — nearest-ETA is a good default and needs no extra machineryLarge buildings / destination-dispatch systems (rider keys in a destination floor before boarding) — the optimal assignment groups riders by destination to minimize total system-wide stops, not just per-request ETA; swap the policy without touching Car/Dispatcher plumbing, same idea as the Parking Lot kata's pluggable FareStrategy

Defend under drilling

You can now defend


Re-authored/Deepened for this guide. Theory: State Pattern and Strategy Pattern — UML, Code & When to Use. Reference implementations (Java + Go) and both break-it demonstrations compiled and run in this session: javac -Xlint:all -Werror clean (state, capacity, dispatcher-selection, FCFS-vs-LOOK, and race assertions all pass, verified across repeated runs); go vet + go build -race clean, same assertions pass — the race detector correctly reports no memory race on the check-then-act bug, which is itself part of the lesson.

🤖 Don't fully get this? Learn it with Claude

Stuck on Design an Elevator System? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Design an Elevator System** (Hands-On Builds) and want to truly understand it. Explain Design an Elevator System from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Design an Elevator System** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Design an Elevator System** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Design an Elevator System** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes