All problems (16)
System Design courseSystem Design Problems

0%

0/16 done

Practice · Low Level Design · State Machines

Design an Elevator System

IntermediateState machineSchedulingObserverStrategy⏱ 2-3 h

Several elevators, many floors, impatient people: the state machine behind each car, and a scheduler that decides which elevator answers a call.

Learn mode: the full solution is shown below.

Problem introduction

DifficultyIntermediate
Time2–3 hours to design and code; about 45 minutes in an interview
PrerequisitesState machines, interfaces, locks
ConceptsA state machine per car, a dispatching Strategy, the LOOK algorithm, simulated time, buttons and a ticker on different goroutines
Real worldLifts in office towers, hospitals and apartment buildings; any system that assigns moving workers to requests (rides, deliveries)

The elevator question checks whether you can split a moving, concurrent system into clean pieces: each car knows how to move and where it has promised to stop, and a dispatcher decides which car answers a call. Interviewers then push on the dispatching rule, because that's where the trade-offs live.

Problem statement

A ten-floor office building in Gulshan has two lifts, A and B. People on a floor press up or down (a hall call); people inside a car press a floor number (a car call). Each car moves one floor per tick, stops at the floors it promised, and opens its doors.

What we are building: the controller that assigns hall calls to cars and moves the cars.

Why it matters: a bad dispatcher sends the far car, or sends a car that's going the other way, and people wait minutes in the lobby.

In one line: assign each hall call to the car that can get there soonest, and move each car efficiently through its stops.

Requirements

Functional requirements

  1. N floors, M cars. Each car has a current floor, a direction (idle, up, down) and a set of stops.
  2. Hall call (floor, direction): pick a car and add the floor to its stops.
  3. Car call (car, floor): add the floor to that car's stops.
  4. Tick: every car moves one floor towards its next stop and opens its doors when it reaches one.
  5. A car keeps going in its direction while there are stops ahead, then turns (no zig-zag).
  6. Refuse floors that don't exist and cars that don't exist.

Non-functional requirements

RequirementWhat it means here
Short waitsThe dispatcher prefers idle cars and cars already heading to the floor in the right direction
ExtensibilitySwapping the dispatching rule (rush-hour mode, VIP floor) doesn't touch the cars
Correctness under concurrencyButtons are pressed on many goroutines while a ticker moves the cars
TestabilityTime is a Tick() call, so tests don't sleep

Assumptions and out of scope

  • Doors open and close within a tick; no capacity limit, no emergency or maintenance mode.

What to hand in

  • Entities, a state diagram for a car, and the dispatching rule.
  • Code for hall calls, car calls and ticks.
  • Tests for dispatching, the LOOK order and concurrency.
Hint 1: who decides?

A car shouldn't decide which hall calls it takes; it only sees itself. Put that decision in one object that sees all cars.

Hint 2: a car's state

Idle, going up, going down. What makes it switch from one to another?

Hint 3: which car is "nearest"?

Distance alone isn't enough. A car two floors away that's going the other way may take much longer than one five floors away coming towards you.

Hint 4: time

Don't use real sleeps. A Tick() method that moves every car one floor makes the whole system testable.

Core entities

EntityJob in one sentenceHoldsDoes
CarMoves through its own stopsname, floor, direction, stopsAddStop, Step (LOOK)
DirectionThe car's stateidle / up / downnothing: an enum
Dispatcher (interface)Picks a car for a hall calldepends on the rulePick(cars, floor, dir)
NearestThe default rule: cheapest car by distance and directionfloors (for the penalty)cost, Pick
BuildingThe entry point; owns the cars and the lockfloors, cars, dispatcherCall, Press, Tick, Idle

Relationships: Building owns its cars and uses a Dispatcher (Strategy). Each Car owns its stops and changes its own direction. Hall calls go through the dispatcher; car calls go straight to the car.

Diagrams

Class diagram

Building- floors: int- cars: []*Car- dispatcher: Dispatcher- mu: sync.Mutex+ Call(floor, dir) (string, error)+ Press(car, floor) error+ Tick() []string+ Idle() (bool, string)Car+ Name: string+ Floor: int+ Dir: Direction- stops: map[int]bool+ AddStop(floor)+ Step() bool- hasStop(dir) bool«interface»Dispatcher+ Pick(cars, floor, dir) *CarNearest+ Floors: int+ Pick(cars, floor, dir) *Car- cost(car, floor, dir) int«enum»DirectionIdleUpDowncars1*dispatcherDirpicksBuilding owns the cars and the one lockBuilding asks a Dispatcher which car takes a callNearest is one rule; a rush-hour rule would be another classeach Car keeps its own direction: idle, up or down
Press play. First Building and the cars it owns, then the Dispatcher it asks, then Nearest as one rule behind that interface, then each car's Direction.

How to read it: Building owns the cars and holds the lock. It doesn't choose cars itself: it holds a Dispatcher and asks it to Pick. Nearest is today's rule; a rush-hour rule would be another box implementing the same interface. Each Car owns its stops and its Direction, which is the small state machine from the LOOK diagram.

Notation: + is public, - is private. A filled diamond ◆ means owns (the part lives and dies with its owner), a plain arrow means holds a reference to, a dashed arrow means uses, and a dashed arrow with a hollow triangle means implements this interface.

Sequence: a hall call, then a tick

PersonTickerBuildingNearestCar BCall(5, down)lockPick(cars, 5, down)A: 22, B: 1car BAddStop(5)"B"Tick()Step()true: doors open at 5
Press play. Floor 5 calls down. Building asks Nearest, which prices both cars (A: 22, B: 1) and returns B. B gets the stop. On the next ticks the ticker calls Tick, B steps and opens its doors at 5.

How to read it: Each vertical dashed line is one object, and time runs downwards. A solid arrow is a call, a dashed arrow is the answer coming back, and the thin bars show when an object is busy. The person and the ticker are two different callers, often on two different goroutines, and both go through Building, which is why it holds the lock. Nearest only answers a question; it never changes a car. The change (AddStop) is made by Building while it holds the lock.

Dispatching a hall call

car A shaftcar B shaftfloor 9floor 8floor 7floor 6floor 5floor 4floor 3floor 2floor 1floor 0A ↑ to 8B ↓ to 1call 5 ↓cost 2 + 20: wrong waycost 1: on its wayBthe dispatcher strategy picks car B; B stops at 5 on its way down
Press play. Floor 5 calls "down". Car A is at 3 going up to 8, so it would have to finish that trip and come back: a big cost. Car B is at 6 going down to 1, and floor 5 is on its way: cost 1. The dispatcher picks B, which stops at 5 on its way down.

How to read it: each shaft is a car. The cost rule charges plain distance for an idle car or a car already heading to the floor in the caller's direction, and adds a penalty (twice the building height) for a car going the other way. The lowest cost wins; ties go to the first car.

LOOK: one direction at a time

012345678928cargoing up: serve 8 firstnothing above: turn and serve 2stops: 8 then 2, one turn
Press play. A car at floor 5 has stops at 8 and 2. It keeps going up to 8 first, then turns and serves 2. One turn, no zig-zag.

How to read it: a car with stops on both sides finishes everything in its current direction before turning. This is the LOOK algorithm (also used for disk heads): it avoids starving far floors and avoids the wasted back-and-forth of always going to the nearest stop.

Design principles

  • Each car owns its movement; the dispatcher owns the choice. Neither reaches into the other's data.
  • Open/closed: a rush-hour dispatcher (send idle cars to the lobby at 9 a.m.) is a new Dispatcher; Car and Building don't change.
  • Time as an input: Tick() is called by a ticker in production and by tests directly.
  • One lock for the shared state: buttons and the ticker both go through Building, which takes the lock.

Design patterns

State (for a car's direction)

A three-value enum with clear transitions: idle → up/down when a stop is added; up ↔ down when nothing is left ahead; up/down → idle when the last stop is served. An enum is enough here; the full State pattern (one class per state) would be overkill for three states with simple behaviour.

Strategy (for dispatching)

  1. Why: the "best car" rule changes with the building and the time of day.
  2. What it solves: dispatching is one swappable object, Dispatcher.
  3. Without it: an if rushHour { … } grows inside Call.
  4. Trade-off: one interface; trivial cost.
  5. Where: dispatcher.go; Building.Call asks b.dispatcher.Pick(...).

Complete solution in Go

Project structure

elevator/
├── go.mod
├── car.go             Car, Direction, the LOOK step
├── dispatcher.go      the Dispatcher strategy and the Nearest rule
├── building.go        Building: Call, Press, Tick, the lock
├── building_test.go   tests, including buttons and ticks at once
└── cmd/demo/main.go   a morning with two lifts

Step 1: the car

// car.go
package elevator

import "sort"

// Direction is the car's state: a tiny state machine (Idle <-> Up/Down).
type Direction int

const (
	Idle Direction = iota
	Up
	Down
)

func (d Direction) String() string { return [...]string{"idle", "up", "down"}[d] }

// Car keeps its own floor, direction and the floors it has promised to stop at.
type Car struct {
	Name  string
	Floor int
	Dir   Direction
	stops map[int]bool
}

func NewCar(name string, floor int) *Car {
	return &Car{Name: name, Floor: floor, stops: map[int]bool{}}
}

func (c *Car) AddStop(floor int) {
	if floor == c.Floor && c.Dir == Idle {
		return
	}
	c.stops[floor] = true
	if c.Dir == Idle {
		c.Dir = c.towards(floor)
	}
}

func (c *Car) towards(floor int) Direction {
	switch {
	case floor > c.Floor:
		return Up
	case floor < c.Floor:
		return Down
	}
	return Idle
}

// hasStop reports whether any stop lies in direction d from the current floor.
func (c *Car) hasStop(d Direction) bool {
	for f := range c.stops {
		if (d == Up && f > c.Floor) || (d == Down && f < c.Floor) {
			return true
		}
	}
	return false
}

// Step moves one floor (the LOOK algorithm: keep going while there are stops ahead, then
// turn). It returns true if the car stopped at a floor it promised.
func (c *Car) Step() bool {
	if len(c.stops) == 0 {
		c.Dir = Idle
		return false
	}
	if !c.hasStop(c.Dir) { // nothing ahead: turn around
		if c.Dir == Up {
			c.Dir = Down
		} else {
			c.Dir = Up
		}
	}
	if c.Dir == Up {
		c.Floor++
	} else {
		c.Floor--
	}
	if c.stops[c.Floor] {
		delete(c.stops, c.Floor)
		if len(c.stops) == 0 {
			c.Dir = Idle
		}
		return true
	}
	return false
}

// Stops lists pending stops in order (for display and tests).
func (c *Car) Stops() []int {
	out := []int{}
	for f := range c.stops {
		out = append(out, f)
	}
	sort.Ints(out)
	return out
}

Step 2: the dispatcher

// dispatcher.go
package elevator

// Dispatcher is the Strategy that decides which car answers a hall call.
type Dispatcher interface {
	Pick(cars []*Car, floor int, dir Direction) *Car
}

// Nearest prefers an idle car or one already heading to the floor in the caller's direction;
// a car going the other way is charged a penalty. Ties go to the first car.
type Nearest struct{ Floors int }

func (n Nearest) cost(c *Car, floor int, dir Direction) int {
	dist := c.Floor - floor
	if dist < 0 {
		dist = -dist
	}
	switch {
	case c.Dir == Idle:
		return dist
	case c.Dir == Up && dir == Up && floor >= c.Floor:
		return dist
	case c.Dir == Down && dir == Down && floor <= c.Floor:
		return dist
	}
	return dist + 2*n.Floors // it must finish its trip and come back
}

func (n Nearest) Pick(cars []*Car, floor int, dir Direction) *Car {
	best := cars[0]
	for _, c := range cars[1:] {
		if n.cost(c, floor, dir) < n.cost(best, floor, dir) {
			best = c
		}
	}
	return best
}

Step 3: the building

// building.go
package elevator

import (
	"fmt"
	"sync"
)

// Building is the entry point: buttons call it from many goroutines, a ticker calls Tick.
type Building struct {
	mu         sync.Mutex
	floors     int
	cars       []*Car
	dispatcher Dispatcher
}

func NewBuilding(floors int, d Dispatcher, cars ...*Car) *Building {
	return &Building{floors: floors, cars: cars, dispatcher: d}
}

// Call is a hall button: someone on `floor` wants to go `dir`. It returns the car assigned.
func (b *Building) Call(floor int, dir Direction) (string, error) {
	b.mu.Lock()
	defer b.mu.Unlock()
	if floor < 0 || floor >= b.floors {
		return "", fmt.Errorf("no floor %d", floor)
	}
	c := b.dispatcher.Pick(b.cars, floor, dir)
	c.AddStop(floor)
	return c.Name, nil
}

// Press is a button inside a car.
func (b *Building) Press(car string, floor int) error {
	b.mu.Lock()
	defer b.mu.Unlock()
	if floor < 0 || floor >= b.floors {
		return fmt.Errorf("no floor %d", floor)
	}
	for _, c := range b.cars {
		if c.Name == car {
			c.AddStop(floor)
			return nil
		}
	}
	return fmt.Errorf("no car %s", car)
}

// Tick moves every car one floor and returns the cars that opened their doors.
func (b *Building) Tick() []string {
	b.mu.Lock()
	defer b.mu.Unlock()
	var opened []string
	for _, c := range b.cars {
		if c.Step() {
			opened = append(opened, fmt.Sprintf("car %s opens at %d", c.Name, c.Floor))
		}
	}
	return opened
}

// Idle reports whether every car has nothing left to do, and where they are.
func (b *Building) Idle() (bool, string) {
	b.mu.Lock()
	defer b.mu.Unlock()
	where := ""
	for i, c := range b.cars {
		if c.Dir != Idle {
			return false, ""
		}
		if i > 0 {
			where += ", "
		}
		where += fmt.Sprintf("%s at %d", c.Name, c.Floor)
	}
	return true, where
}

Step 4: run it

// cmd/demo/main.go
package main

import (
	"fmt"

	"elevator"
)

func main() {
	b := elevator.NewBuilding(10, elevator.Nearest{Floors: 10},
		elevator.NewCar("A", 0), elevator.NewCar("B", 9))

	call := func(t, floor int, dir elevator.Direction) {
		car, _ := b.Call(floor, dir)
		fmt.Printf("t%-2d call floor %d %-4s -> car %s\n", t, floor, dir, car)
	}
	press := func(t int, car string, floor int) {
		b.Press(car, floor)
		fmt.Printf("t%-2d car %s: passenger goes to %d\n", t, car, floor)
	}

	call(0, 3, elevator.Up)
	call(0, 7, elevator.Down)
	for t := 1; t <= 20; t++ {
		for _, e := range b.Tick() {
			fmt.Printf("t%-2d %s\n", t, e)
		}
		switch t { // what people do at each moment of the story
		case 2:
			press(t, "B", 1)
		case 3:
			press(t, "A", 8)
			call(t, 5, elevator.Down)
		}
		if idle, where := b.Idle(); idle {
			fmt.Printf("t%-2d all cars idle: %s\n", t, where)
			return
		}
	}
}
$ go run ./cmd/demo
t0  call floor 3 up   -> car A
t0  call floor 7 down -> car B
t2  car B opens at 7
t2  car B: passenger goes to 1
t3  car A opens at 3
t3  car A: passenger goes to 8
t3  call floor 5 down -> car B
t4  car B opens at 5
t8  car A opens at 8
t8  car B opens at 1
t8  all cars idle: A at 8, B at 1

Reading the output:

  • t0: floor 3 up goes to A (3 floors away, idle); floor 7 down goes to B (2 floors away, idle).
  • t2: B reaches 7; its passenger presses 1.
  • t3: A reaches 3; its passenger presses 8. Then floor 5 calls down: A is going up (cost 2 + 20), B is at 6 going down (cost 1), so B gets it.
  • t4: B stops at 5 on the way.
  • t8: A reaches 8 and B reaches 1 on the same tick; both are idle.

The other five languages

from __future__ import annotations

import threading
from enum import Enum
from typing import Protocol


class Dir(Enum):
    """The car's state: a tiny state machine (IDLE <-> UP/DOWN)."""
    IDLE = "idle"
    UP = "up"
    DOWN = "down"


class Car:
    def __init__(self, name: str, floor: int):
        self.name, self.floor, self.dir = name, floor, Dir.IDLE
        self.stops: set[int] = set()

    def add_stop(self, floor: int) -> None:
        if floor == self.floor and self.dir is Dir.IDLE:
            return
        self.stops.add(floor)
        if self.dir is Dir.IDLE:
            self.dir = Dir.UP if floor > self.floor else Dir.DOWN

    def _has_stop(self, d: Dir) -> bool:
        return any((d is Dir.UP and f > self.floor) or (d is Dir.DOWN and f < self.floor) for f in self.stops)

    def step(self) -> bool:
        """LOOK: keep going while there are stops ahead, then turn. True if it stopped."""
        if not self.stops:
            self.dir = Dir.IDLE
            return False
        if not self._has_stop(self.dir):
            self.dir = Dir.DOWN if self.dir is Dir.UP else Dir.UP
        self.floor += 1 if self.dir is Dir.UP else -1
        if self.floor in self.stops:
            self.stops.remove(self.floor)
            if not self.stops:
                self.dir = Dir.IDLE
            return True
        return False


class Dispatcher(Protocol):
    """The Strategy that decides which car answers a hall call."""
    def pick(self, cars: list[Car], floor: int, d: Dir) -> Car: ...


class Nearest:
    def __init__(self, floors: int):
        self.floors = floors

    def cost(self, c: Car, floor: int, d: Dir) -> int:
        dist = abs(c.floor - floor)
        if c.dir is Dir.IDLE:
            return dist
        if c.dir is Dir.UP and d is Dir.UP and floor >= c.floor:
            return dist
        if c.dir is Dir.DOWN and d is Dir.DOWN and floor <= c.floor:
            return dist
        return dist + 2 * self.floors  # it must finish its trip and come back

    def pick(self, cars: list[Car], floor: int, d: Dir) -> Car:
        return min(cars, key=lambda c: self.cost(c, floor, d))  # min() keeps the first on ties


class Building:
    def __init__(self, floors: int, dispatcher: Dispatcher, *cars: Car):
        self._lock = threading.Lock()
        self.floors, self.dispatcher, self.cars = floors, dispatcher, list(cars)

    def call(self, floor: int, d: Dir) -> str:
        with self._lock:
            if not 0 <= floor < self.floors:
                raise ValueError(f"no floor {floor}")
            c = self.dispatcher.pick(self.cars, floor, d)
            c.add_stop(floor)
            return c.name

    def press(self, car: str, floor: int) -> None:
        with self._lock:
            next(c for c in self.cars if c.name == car).add_stop(floor)

    def tick(self) -> list[str]:
        with self._lock:
            return [f"car {c.name} opens at {c.floor}" for c in self.cars if c.step()]

    def idle(self) -> tuple[bool, str]:
        with self._lock:
            if any(c.dir is not Dir.IDLE for c in self.cars):
                return False, ""
            return True, ", ".join(f"{c.name} at {c.floor}" for c in self.cars)


b = Building(10, Nearest(10), Car("A", 0), Car("B", 9))


def call(t: int, floor: int, d: Dir) -> None:
    car = b.call(floor, d)
    print(f"t{t:<2} call floor {floor} {d.value:<4} -> car {car}")


def press(t: int, car: str, floor: int) -> None:
    b.press(car, floor)
    print(f"t{t:<2} car {car}: passenger goes to {floor}")


call(0, 3, Dir.UP)
call(0, 7, Dir.DOWN)
for t in range(1, 21):
    for e in b.tick():
        print(f"t{t:<2} {e}")
    if t == 2:
        press(t, "B", 1)
    elif t == 3:
        press(t, "A", 8)
        call(t, 5, Dir.DOWN)
    done, where = b.idle()
    if done:
        print(f"t{t:<2} all cars idle: {where}")
        break

What changes from language to language: Java and C++ use ordered sets (TreeSet, std::set) so "is there a stop above me?" is one call (higher, upper_bound). Rust's dispatcher returns an index instead of a reference, so it can pick a car and then mutate it under the same lock. Rust's match with guards ((Dir::Up, Dir::Up) if floor >= c.floor) reads almost like the cost table. TypeScript needs no lock.

Testing

// building_test.go
package elevator

import (
	"slices"
	"sync"
	"testing"
)

func TestNearestIdleCarWins(t *testing.T) {
	b := NewBuilding(10, Nearest{10}, NewCar("A", 0), NewCar("B", 9))
	if car, _ := b.Call(7, Down); car != "B" {
		t.Fatalf("got %s", car)
	}
	if car, _ := b.Call(1, Up); car != "A" {
		t.Fatalf("got %s", car)
	}
}

func TestCarGoingTheWrongWayIsAvoided(t *testing.T) {
	a, b := NewCar("A", 4), NewCar("B", 0)
	a.AddStop(9) // A is going up, past floor 3
	bld := NewBuilding(10, Nearest{10}, a, b)
	if car, _ := bld.Call(3, Down); car != "B" {
		t.Fatalf("A is heading away; got %s", car)
	}
}

func TestLookServesStopsAheadFirst(t *testing.T) {
	c := NewCar("A", 5)
	c.AddStop(8) // going up
	c.AddStop(2) // behind it
	var order []int
	for range 20 {
		if c.Step() {
			order = append(order, c.Floor)
		}
	}
	if !slices.Equal(order, []int{8, 2}) || c.Dir != Idle {
		t.Fatalf("order %v, dir %v", order, c.Dir)
	}
}

func TestInvalidFloorRefused(t *testing.T) {
	b := NewBuilding(10, Nearest{10}, NewCar("A", 0))
	if _, err := b.Call(12, Up); err == nil {
		t.Fatal("floor 12 should be refused")
	}
	if err := b.Press("Z", 3); err == nil {
		t.Fatal("unknown car should be refused")
	}
}

func TestEveryCallIsEventuallyServed(t *testing.T) {
	b := NewBuilding(10, Nearest{10}, NewCar("A", 0), NewCar("B", 9))
	for _, f := range []int{2, 5, 8, 1, 6} {
		b.Call(f, Up)
	}
	for range 40 {
		b.Tick()
	}
	if idle, _ := b.Idle(); !idle {
		t.Fatal("cars still busy after 40 ticks")
	}
}

// Buttons pressed from many goroutines while the ticker runs. Run with -race.
func TestConcurrentButtonsAndTicks(t *testing.T) {
	b := NewBuilding(10, Nearest{10}, NewCar("A", 0), NewCar("B", 9))
	var wg sync.WaitGroup
	for i := range 30 {
		wg.Add(2)
		go func() { defer wg.Done(); b.Call(i%10, Up) }()
		go func() { defer wg.Done(); b.Tick() }()
	}
	wg.Wait()
	for range 60 {
		b.Tick()
	}
	if idle, _ := b.Idle(); !idle {
		t.Fatal("not idle")
	}
}
$ go test -race -v ./...
=== RUN   TestNearestIdleCarWins
--- PASS: TestNearestIdleCarWins (0.00s)
=== RUN   TestCarGoingTheWrongWayIsAvoided
--- PASS: TestCarGoingTheWrongWayIsAvoided (0.00s)
=== RUN   TestLookServesStopsAheadFirst
--- PASS: TestLookServesStopsAheadFirst (0.00s)
=== RUN   TestInvalidFloorRefused
--- PASS: TestInvalidFloorRefused (0.00s)
=== RUN   TestEveryCallIsEventuallyServed
--- PASS: TestEveryCallIsEventuallyServed (0.00s)
=== RUN   TestConcurrentButtonsAndTicks
--- PASS: TestConcurrentButtonsAndTicks (0.00s)
PASS
ok  	elevator	1.015s
?   	elevator/cmd/demo	[no test files]
TestChecks
TestNearestIdleCarWinsCalls go to the nearest idle car
TestCarGoingTheWrongWayIsAvoidedA closer car heading away loses to a farther idle one
TestLookServesStopsAheadFirstFrom 5 with stops 8 and 2, the order is 8 then 2
TestInvalidFloorRefusedFloor 12 and an unknown car are refused
TestEveryCallIsEventuallyServedFive calls, and both cars are idle within 40 ticks
TestConcurrentButtonsAndTicksCalls and ticks from many goroutines at once

Without the lock in Tick, the race detector reported 28 data races and the concurrency test failed.

Design review

What's good: clean split between movement (car) and choice (dispatcher); time is testable; the cost rule is easy to explain and change.

"What if…"What to change
Rush hourA new dispatcher that sends idle cars to the lobby and favours up-calls from floor 0.
Car capacityCars report load; the dispatcher skips full cars.
Destination dispatch (pick floor in the lobby)Calls carry the destination, so the dispatcher can group people going to nearby floors.
Re-assigning callsIf a better car frees up, move the hall call; keep hall calls in the building, not only in cars.
Emergency / fire modeA building-wide state that sends every car to the ground floor and ignores calls.

Interview questions

Practice questions

Beginner

  1. Why does the dispatcher, not the car, decide which car takes a call?
  2. What are a car's states, and what moves it between them?

Intermediate

  1. Why isn't "closest car" enough?
  2. How do you test a moving system without sleeping?

Advanced

  1. Compare LOOK with always going to the nearest stop.
  2. Buttons and the ticker run on different goroutines. What can go wrong?

Common design mistakes

  • Each car grabbing calls on its own, so two cars come for one person.
  • Pure distance dispatching, sending a car that's going the other way.
  • Real time.Sleep in the logic, making tests slow and flaky.
  • No lock between buttons and the ticker.

Answers

1. Why the dispatcher?

Only something that sees every car can compare them. If cars chose for themselves, two might take the same call, or none.

2. A car's states?

Idle, up, down. Adding a stop moves idle to up or down; serving the last stop ahead turns it around if there are stops behind, or makes it idle if there are none.

3. Why not just the closest car?

A close car heading away must finish its trip first. The cost rule adds a penalty for that, so a farther idle or approaching car can win.

4. Testing without sleeping?

Make time an explicit Tick(). Tests call it in a loop and check where each car is; production calls it from a ticker.

5. LOOK vs nearest stop?

Nearest-stop can bounce between nearby floors and starve far ones. LOOK finishes one direction before turning: predictable, fair, and fewer reversals.

6. Buttons vs ticker without a lock?

A call could add a stop while Step is deleting from the same map (a data race, and in Go a possible crash), or two calls could be assigned using a car's stale state. Our test reported 28 data races without the lock.

What interviewers look for: car vs dispatcher responsibilities, a sensible cost rule, LOOK, testable time, and thread safety.

Designed it yourself and understood the solution? Mark it to track your progress.