Low Level DesignWorked problems
Design an elevator system
Design an elevator system step by step for a low level design interview. Grow from one car to a sweep that never starves a floor, many cars with a dispatcher, broken and full cars, and a single control loop, with code in Java, Python, C#, Go and TypeScript.
- Related
- Parking lotTask scheduler
“Design an elevator system for a building.” Usually followed, ten minutes in, by “now there are four elevators”.
The elevator question hides two different problems under one prompt. The first is inside one car: in what order should it visit the floors it has to stop at? The second is across cars: when someone presses a button on floor 12, which car should go?
Most answers mix the two into one big ElevatorController with an endless loop and a sleep. Then
they can’t explain why a person on floor 19 waits forever during the lunch rush.
The answer in one minute
Section titled “The answer in one minute”If you only have a minute, this is the design. The rest of the page explains how we get here.
- Time is a method, not a sleeping thread. Each call to
step()moves the system forward one tick. A test can run an hour of traffic in a few microseconds. - Each car sweeps. It keeps going in one direction while it has stops ahead, then turns around. This is called LOOK, and it means nobody waits more than about two trips across the building.
- A button on a floor carries a direction. “Down on floor 5” is only served by a car going down. That’s why there are two buttons.
- A dispatcher chooses the car. It picks the car that would arrive soonest, counting the detour a car must make if it’s heading the wrong way. The rule can be swapped per building.
- The building remembers every call it accepted. So a broken car hands its calls to other cars, and pressing a lit button again does nothing. And one control loop owns everything, so button presses from many threads can’t corrupt it.
We’ll build this in four stages, then deal with buttons pressed from many threads at once.
Scope it
Section titled “Scope it”Before drawing anything, we ask a few questions. Each answer changes a class. Anything that doesn’t, we assume and move on.
Design an elevator system for a building.
Whether you pin down the shape (floors, cars, up and down buttons or a keypad) in about three minutes, and ask what “good” means: average wait, worst wait, or energy. Jumping straight to classes means you’ll improve something nobody asked for.
Here are the questions that matter:
- How many floors and cars? Tens of floors, a handful of cars. With one car, there’s no dispatcher. With hundreds of floors, splitting the building into zones becomes worth it.
- Up and down buttons, or a keypad where riders type their floor? Up and down buttons on each floor. A keypad is an extension at the end.
- What are we improving? First, nobody waits forever. Then, short average waits. If only the average mattered, “nearest stop first” would win, and it can leave some floors waiting forever.
- Do we control the motors? No. We decide where cars go. Motors and doors are hardware, so we don’t model acceleration or door timing.
- Can cars break or fill up? Yes. A car can be taken out of service, and a sensor says when a car is full. Perfect cars would remove stage 4.
These answers give us one promise that matters more than the others: no call waits forever, whatever the traffic. It’s decided by the order of stops inside each car. Three smaller promises come with it. Button presses from many threads at once never corrupt a car. The rule for choosing a car can change without editing the cars. And time is passed in, so a test can check every tick.
We leave out motor control, door timing in seconds, and the alarm and intercom.
Go deeperThree more questions, and what each changes1 min
| Question | We assume | If the answer were different |
|---|---|---|
| Do all cars serve all floors? | Yes | Express or zoned cars filter which cars the dispatcher can pick |
| Special modes: fire service, VIP? | Mentioned, not built | Each mode changes several behaviors at once; that’s where State pays off |
| Morning and evening rush hours? | One dispatching rule for now | Rush-hour rules are a second dispatcher, switched by time of day |
Build it
Section titled “Build it”We’ll build the design in four stages. Each stage adds only the classes that a new requirement needs. In each diagram, the new parts are outlined, and the colors show what each class does (the key is under every diagram).
Stage 1: One car, one queue
Section titled “Stage 1: One car, one queue”Start with one elevator. Someone presses a button. What happens?
Whether you make time explicit (a step that moves forward one tick) instead of a thread that
sleeps, and say why. while (true) { move(); sleep(1000); } is the answer they hear most, and
it can’t be tested without waiting in real time.
Start with the smallest thing that works: one Car, with a floor, a door that’s open or closed,
and a queue of requested floors.
The important decision is about time. Elevators move on a clock. The tempting model is a thread that moves the car and then sleeps for a second. But that ties every test to the real clock. Checking a ten-floor trip takes ten seconds, and checking two things happening at once takes luck.
So instead, time is a method. Each call to step() is one tick, and a tick does exactly one
thing: close the doors, open them at a requested floor, or move one floor. Whoever owns the car
decides how often to call it. In production, that’s a loop running every half second. In a test,
it’s a for loop that runs a whole morning rush in microseconds.
- Color key
- Facade
- Entity
- Value object
- Enum
- Interface
- Implementation
- New in this step
// One car in one shaft. Time moves only when someone calls step(), one tick at a time,// so a test can run a whole morning rush in microseconds and check every tick.class Car { private int floor; // floors are numbered from 0 private boolean doorsOpen; // flaw 1: first come, first served. Requests for 9, 1, 8, 2 send the car up and down // the whole shaft four times, and it passes floor 2 twice without stopping. private final Deque<Integer> requests = new ArrayDeque<>();
// flaw 2: a request is only a floor. Someone on 5 who wants to go down is picked up by a // car on its way up, and rides to the top first. Stage 2 gives hall calls a direction. void request(int f) { if (!requests.contains(f)) requests.addLast(f); // pressing twice is one request }
// One tick does exactly one thing: close the doors, open them at a requested floor, // or move one floor toward the next request. void step() { if (doorsOpen) { doorsOpen = false; return; } if (requests.isEmpty()) return; int target = requests.peekFirst(); if (target == floor) { requests.removeFirst(); doorsOpen = true; return; } floor += target > floor ? 1 : -1; }
int floor() { return floor; } boolean doorsOpen() { return doorsOpen; }}from collections import deque
# One car in one shaft. Time moves only when someone calls step(), one tick at a time,# so a test can run a whole morning rush in microseconds and check every tick.class Car: def __init__(self): self.floor = 0 # floors are numbered from 0 self.doors_open = False # flaw 1: first come, first served. Requests for 9, 1, 8, 2 send the car up and down # the whole shaft four times, and it passes floor 2 twice without stopping. self._requests: deque[int] = deque()
# flaw 2: a request is only a floor. Someone on 5 who wants to go down is picked up by a # car on its way up, and rides to the top first. Stage 2 gives hall calls a direction. def request(self, f: int) -> None: if f not in self._requests: # pressing twice is one request self._requests.append(f)
# One tick does exactly one thing: close the doors, open them at a requested floor, # or move one floor toward the next request. def step(self) -> None: if self.doors_open: self.doors_open = False return if not self._requests: return target = self._requests[0] if target == self.floor: self._requests.popleft() self.doors_open = True return self.floor += 1 if target > self.floor else -1// One car in one shaft. Time moves only when someone calls Step(), one tick at a time,// so a test can run a whole morning rush in microseconds and check every tick.public sealed class Car{ public int Floor { get; private set; } // floors are numbered from 0 public bool DoorsOpen { get; private set; }
// flaw 1: first come, first served. Requests for 9, 1, 8, 2 send the car up and down // the whole shaft four times, and it passes floor 2 twice without stopping. private readonly LinkedList<int> _requests = new();
// flaw 2: a request is only a floor. Someone on 5 who wants to go down is picked up by a // car on its way up, and rides to the top first. Stage 2 gives hall calls a direction. public void Request(int f) { if (!_requests.Contains(f)) _requests.AddLast(f); // pressing twice is one request }
// One tick does exactly one thing: close the doors, open them at a requested floor, // or move one floor toward the next request. public void Step() { if (DoorsOpen) { DoorsOpen = false; return; } if (_requests.First is not { } next) return; if (next.Value == Floor) { _requests.RemoveFirst(); DoorsOpen = true; return; } Floor += next.Value > Floor ? 1 : -1; }}// Car is one car in one shaft. Time moves only when someone calls Step, one tick at a time,// so a test can run a whole morning rush in microseconds and check every tick.type Car struct { Floor int // floors are numbered from 0 DoorsOpen bool // flaw 1: first come, first served. Requests for 9, 1, 8, 2 send the car up and down // the whole shaft four times, and it passes floor 2 twice without stopping. requests []int}
// Request is only a floor. flaw 2: someone on 5 who wants to go down is picked up by a car// on its way up, and rides to the top first. Stage 2 gives hall calls a direction.func (c *Car) Request(f int) { if !slices.Contains(c.requests, f) { // pressing twice is one request c.requests = append(c.requests, f) }}
// Step does exactly one thing: close the doors, open them at a requested floor,// or move one floor toward the next request.func (c *Car) Step() { if c.DoorsOpen { c.DoorsOpen = false return } if len(c.requests) == 0 { return } target := c.requests[0] switch { case target == c.Floor: c.requests = c.requests[1:] c.DoorsOpen = true case target > c.Floor: c.Floor++ default: c.Floor-- }}// One car in one shaft. Time moves only when someone calls step(), one tick at a time,// so a test can run a whole morning rush in microseconds and check every tick.class Car { floor = 0; // floors are numbered from 0 doorsOpen = false; // flaw 1: first come, first served. Requests for 9, 1, 8, 2 send the car up and down // the whole shaft four times, and it passes floor 2 twice without stopping. private readonly requests: number[] = [];
// flaw 2: a request is only a floor. Someone on 5 who wants to go down is picked up by a // car on its way up, and rides to the top first. Stage 2 gives hall calls a direction. request(f: number): void { if (!this.requests.includes(f)) this.requests.push(f); // pressing twice is one request }
// One tick does exactly one thing: close the doors, open them at a requested floor, // or move one floor toward the next request. step(): void { if (this.doorsOpen) { this.doorsOpen = false; return; } if (this.requests.length === 0) return; const target = this.requests[0]; if (target === this.floor) { this.requests.shift(); this.doorsOpen = true; return; } this.floor += target > this.floor ? 1 : -1; }}It works, but it has two problems, and we leave them in on purpose. Floors are served in the order they were pressed. So presses for 9, 1, 8 and 2 send the car up and down the whole building four times, passing floor 2 twice. And a request is just a floor number. So someone on floor 5 who wants to go down gets picked up by a car on its way to the top.
Stage 2: Direction and the sweep
Section titled “Stage 2: Direction and the sweep”Your car zig-zags. In what order should it serve its stops?
Whether you know the sweep (keep going while there’s work ahead, then turn around) and can say why “nearest first” is wrong: it can leave floors waiting forever. Naming SCAN or LOOK is a plus. Explaining why nearest-first is unfair is what they’re really listening for.
Disk drives had the same problem in the 1970s: a reading head moving along a line, with requests arriving at points on it. The options are well known:
| Order | How it picks the next stop | Can a floor wait forever? | Notes |
|---|---|---|---|
| First come, first served | Press order | No, but it wastes travel | Stage 1 |
| Nearest first | The closest stop, either way | Yes: far floors can starve | Looks best, is unfair |
| SCAN | Sweep end to end, turn around at the last floor | No | Travels to the end even if nobody is there |
| LOOK (our choice) | Sweep while stops remain ahead, then turn around | No: at most two sweeps | What real elevators do |
Why does “nearest first” fail? Picture steady traffic between floors 1 and 3. There’s always a stop close by, so the car never goes up to floor 19. The person there waits forever. That’s called starvation.
LOOK is the elevator algorithm. A car going up keeps going up while any stop remains above it, then turns around. A rider ahead of the car, in its direction, is served on this sweep. Anyone else is served on the next one. So no wait is longer than about two trips across the building.
Doing this right needs one more fact: a floor button has a direction. Pressing “down” on floor 5 is a request to be picked up by a car going down. So stops come in three kinds:
- A car stop (a button inside the car) is served whichever way the car is moving.
- An up call is served only by a car going up.
- A down call is served only by a car going down.
There’s one exception: the top of a sweep. A car going up, with nothing above it, can take a down call right where it is, because down is where it goes next anyway. Each kind of stop is kept in a sorted set, so “is there anything above me?” is one quick lookup.
step() now also returns the floor calls it answered. The building uses that to switch off the
button’s light, and later stages use it to know which calls are still waiting.
- Color key
- Facade
- Entity
- Value object
- Enum
- Interface
- Implementation
- New in this step
// Which way a car is heading. IDLE means it has nothing left to do.enum Direction { UP, DOWN, IDLE }
// A button pressed on a floor. It is a promise to arrive *going that way*, which is// why it carries a direction and a button inside the car does not.record HallCall(int floor, Direction direction) {}
class Car { final int id; private final int floors; // floors are 0 .. floors - 1 private int floor; private Direction direction = Direction.IDLE; private boolean doorsOpen; // Three kinds of stop, each a sorted set so "anything above me?" is one lookup. // A car stop is served whichever way the car is going. An up call only by a car going up. private final TreeSet<Integer> carStops = new TreeSet<>(); private final TreeSet<Integer> upCalls = new TreeSet<>(); private final TreeSet<Integer> downCalls = new TreeSet<>();
Car(int id, int floors) { this.id = id; this.floors = floors; }
void carCall(int f) { check(f); carStops.add(f); }
void hallCall(HallCall c) { check(c.floor()); (c.direction() == Direction.UP ? upCalls : downCalls).add(c.floor()); }
// One tick: close the doors, or open them for the stops here, or move one floor. // Returns the hall calls answered this tick, so the building can turn their lights off. List<HallCall> step() { if (doorsOpen) { doorsOpen = false; return List.of(); } boolean carStop = carStops.remove(floor); // before serveHere, which checks what is left above List<HallCall> served = serveHere(); if (carStop || !served.isEmpty()) { doorsOpen = true; return served; } direction = nextDirection(); if (direction == Direction.UP) floor++; else if (direction == Direction.DOWN) floor--; return List.of(); }
// The hall calls this floor can answer, given the way the car is heading. private List<HallCall> serveHere() { List<HallCall> served = new ArrayList<>(); if (direction != Direction.DOWN && upCalls.remove(floor)) { served.add(new HallCall(floor, Direction.UP)); direction = Direction.UP; } else if (direction != Direction.UP && downCalls.remove(floor)) { served.add(new HallCall(floor, Direction.DOWN)); direction = Direction.DOWN; } else if (direction == Direction.UP && highestStop() <= floor && downCalls.remove(floor)) { // The top of this sweep: nothing above, so the down call here is next anyway. served.add(new HallCall(floor, Direction.DOWN)); direction = Direction.DOWN; } else if (direction == Direction.DOWN && lowestStop() >= floor && upCalls.remove(floor)) { served.add(new HallCall(floor, Direction.UP)); direction = Direction.UP; } return served; }
// LOOK: keep going while there is work ahead, then turn around. Every stop in the car's // way is served on this sweep, and everything else on the next, so no call waits forever. private Direction nextDirection() { boolean above = highestStop() > floor, below = lowestStop() < floor; if (direction == Direction.UP && above) return Direction.UP; if (direction == Direction.DOWN && below) return Direction.DOWN; if (above) return Direction.UP; if (below) return Direction.DOWN; return Direction.IDLE; }
// The farthest stops in each direction. With no stops, both are the current floor. int highestStop() { return max(max(max(floor, carStops), upCalls), downCalls); } int lowestStop() { return min(min(min(floor, carStops), upCalls), downCalls); }
private static int max(int f, TreeSet<Integer> s) { return s.isEmpty() ? f : Math.max(f, s.last()); } private static int min(int f, TreeSet<Integer> s) { return s.isEmpty() ? f : Math.min(f, s.first()); }
private void check(int f) { if (f < 0 || f >= floors) throw new IllegalArgumentException("no floor " + f); }
int floor() { return floor; } Direction direction() { return direction; } boolean doorsOpen() { return doorsOpen; }}from dataclasses import dataclassfrom enum import Enum
# Which way a car is heading. IDLE means it has nothing left to do.class Direction(Enum): UP = "up" DOWN = "down" IDLE = "idle"
# A button pressed on a floor. It is a promise to arrive *going that way*, which is# why it carries a direction and a button inside the car does not.@dataclass(frozen=True)class HallCall: floor: int direction: Direction
class Car: def __init__(self, id: int, floors: int): self.id = id self._floors = floors # floors are 0 .. floors - 1 self.floor = 0 self.direction = Direction.IDLE self.doors_open = False # Three kinds of stop. A car stop is served whichever way the car is going; # an up call only by a car going up. Plain sets: max() and min() over a handful of # floors is cheap, and a sorted container would add a dependency for nothing. self._car_stops: set[int] = set() self._up_calls: set[int] = set() self._down_calls: set[int] = set()
def car_call(self, f: int) -> None: self._check(f) self._car_stops.add(f)
def hall_call(self, c: HallCall) -> None: self._check(c.floor) (self._up_calls if c.direction is Direction.UP else self._down_calls).add(c.floor)
# One tick: close the doors, or open them for the stops here, or move one floor. # Returns the hall calls answered this tick, so the building can turn their lights off. def step(self) -> list[HallCall]: if self.doors_open: self.doors_open = False return [] car_stop = self.floor in self._car_stops # before _serve_here, which checks what is left above self._car_stops.discard(self.floor) served = self._serve_here() if car_stop or served: self.doors_open = True return served self.direction = self._next_direction() if self.direction is Direction.UP: self.floor += 1 elif self.direction is Direction.DOWN: self.floor -= 1 return []
# The hall calls this floor can answer, given the way the car is heading. def _serve_here(self) -> list[HallCall]: f, d = self.floor, self.direction if d is not Direction.DOWN and f in self._up_calls: return self._take(self._up_calls, Direction.UP) if d is not Direction.UP and f in self._down_calls: return self._take(self._down_calls, Direction.DOWN) # The top of this sweep: nothing above, so the down call here is next anyway. if d is Direction.UP and self.highest_stop() <= f and f in self._down_calls: return self._take(self._down_calls, Direction.DOWN) if d is Direction.DOWN and self.lowest_stop() >= f and f in self._up_calls: return self._take(self._up_calls, Direction.UP) return []
def _take(self, calls: set[int], d: Direction) -> list[HallCall]: calls.discard(self.floor) self.direction = d return [HallCall(self.floor, d)]
# LOOK: keep going while there is work ahead, then turn around. Every stop in the car's # way is served on this sweep, and everything else on the next, so no call waits forever. def _next_direction(self) -> Direction: above, below = self.highest_stop() > self.floor, self.lowest_stop() < self.floor if self.direction is Direction.UP and above: return Direction.UP if self.direction is Direction.DOWN and below: return Direction.DOWN if above: return Direction.UP if below: return Direction.DOWN return Direction.IDLE
# The farthest stops in each direction. With no stops, both are the current floor. def highest_stop(self) -> int: return max([self.floor, *self._car_stops, *self._up_calls, *self._down_calls])
def lowest_stop(self) -> int: return min([self.floor, *self._car_stops, *self._up_calls, *self._down_calls])
def _check(self, f: int) -> None: if not 0 <= f < self._floors: raise ValueError(f"no floor {f}")// Which way a car is heading. Idle means it has nothing left to do.public enum Direction { Up, Down, Idle }
// A button pressed on a floor. It is a promise to arrive *going that way*, which is// why it carries a direction and a button inside the car does not.public readonly record struct HallCall(int Floor, Direction Direction);
public sealed class Car(int id, int floors) // floors are 0 .. floors - 1{ public int Id { get; } = id; public int Floor { get; private set; } public Direction Direction { get; private set; } = Direction.Idle; public bool DoorsOpen { get; private set; }
// Three kinds of stop, each a sorted set so "anything above me?" is one lookup. // A car stop is served whichever way the car is going. An up call only by a car going up. private readonly SortedSet<int> _carStops = []; private readonly SortedSet<int> _upCalls = []; private readonly SortedSet<int> _downCalls = [];
public void CarCall(int f) { Check(f); _carStops.Add(f); }
public void HallCall(HallCall c) { Check(c.Floor); (c.Direction == Direction.Up ? _upCalls : _downCalls).Add(c.Floor); }
// One tick: close the doors, or open them for the stops here, or move one floor. // Returns the hall calls answered this tick, so the building can turn their lights off. public IReadOnlyList<HallCall> Step() { if (DoorsOpen) { DoorsOpen = false; return []; } bool carStop = _carStops.Remove(Floor); // before ServeHere, which checks what is left above var served = ServeHere(); if (carStop || served.Count > 0) { DoorsOpen = true; return served; } Direction = NextDirection(); if (Direction == Direction.Up) Floor++; else if (Direction == Direction.Down) Floor--; return []; }
// The hall calls this floor can answer, given the way the car is heading. private List<HallCall> ServeHere() { if (Direction != Direction.Down && _upCalls.Contains(Floor)) return Take(_upCalls, Direction.Up); if (Direction != Direction.Up && _downCalls.Contains(Floor)) return Take(_downCalls, Direction.Down); // The top of this sweep: nothing above, so the down call here is next anyway. if (Direction == Direction.Up && HighestStop <= Floor && _downCalls.Contains(Floor)) return Take(_downCalls, Direction.Down); if (Direction == Direction.Down && LowestStop >= Floor && _upCalls.Contains(Floor)) return Take(_upCalls, Direction.Up); return []; }
private List<HallCall> Take(SortedSet<int> calls, Direction d) { calls.Remove(Floor); Direction = d; return [new HallCall(Floor, d)]; }
// LOOK: keep going while there is work ahead, then turn around. Every stop in the car's // way is served on this sweep, and everything else on the next, so no call waits forever. private Direction NextDirection() { bool above = HighestStop > Floor, below = LowestStop < Floor; if (Direction == Direction.Up && above) return Direction.Up; if (Direction == Direction.Down && below) return Direction.Down; if (above) return Direction.Up; if (below) return Direction.Down; return Direction.Idle; }
// The farthest stops in each direction. With no stops, both are the current floor. public int HighestStop => Math.Max(Floor, Math.Max(Max(_carStops), Math.Max(Max(_upCalls), Max(_downCalls)))); public int LowestStop => Math.Min(Floor, Math.Min(Min(_carStops), Math.Min(Min(_upCalls), Min(_downCalls))));
private static int Max(SortedSet<int> s) => s.Count == 0 ? int.MinValue : s.Max; private static int Min(SortedSet<int> s) => s.Count == 0 ? int.MaxValue : s.Min;
private void Check(int f) { if (f < 0 || f >= floors) throw new ArgumentOutOfRangeException(nameof(f), $"no floor {f}"); }}// Direction is which way a car is heading. Idle means it has nothing left to do.type Direction int
const ( Idle Direction = iota Up Down)
// HallCall is a button pressed on a floor. It is a promise to arrive going that way,// which is why it carries a direction and a button inside the car does not.type HallCall struct { Floor int Direction Direction}
type Car struct { ID int floors int // floors are 0 .. floors-1 Floor int Direction Direction DoorsOpen bool // Three kinds of stop. A car stop is served whichever way the car is going; // an up call only by a car going up. Maps as sets: a building has tens of floors, // so scanning them for the highest stop costs nothing. carStops, upCalls, downCalls map[int]bool}
func NewCar(id, floors int) *Car { return &Car{ID: id, floors: floors, carStops: map[int]bool{}, upCalls: map[int]bool{}, downCalls: map[int]bool{}}}
func (c *Car) CarCall(f int) error { if err := c.check(f); err != nil { return err } c.carStops[f] = true return nil}
func (c *Car) HallCall(h HallCall) error { if err := c.check(h.Floor); err != nil { return err } if h.Direction == Up { c.upCalls[h.Floor] = true } else { c.downCalls[h.Floor] = true } return nil}
// Step does one thing: close the doors, or open them for the stops here, or move one floor.// It returns the hall calls answered this tick, so the building can turn their lights off.func (c *Car) Step() []HallCall { if c.DoorsOpen { c.DoorsOpen = false return nil } carStop := c.carStops[c.Floor] // before serveHere, which checks what is left above delete(c.carStops, c.Floor) served := c.serveHere() if carStop || len(served) > 0 { c.DoorsOpen = true return served } c.Direction = c.nextDirection() switch c.Direction { case Up: c.Floor++ case Down: c.Floor-- } return nil}
// serveHere answers the hall calls this floor can take, given the way the car is heading.func (c *Car) serveHere() []HallCall { f, d := c.Floor, c.Direction switch { case d != Down && c.upCalls[f]: return c.take(c.upCalls, Up) case d != Up && c.downCalls[f]: return c.take(c.downCalls, Down) case d == Up && c.HighestStop() <= f && c.downCalls[f]: // The top of this sweep: nothing above, so the down call here is next anyway. return c.take(c.downCalls, Down) case d == Down && c.LowestStop() >= f && c.upCalls[f]: return c.take(c.upCalls, Up) } return nil}
func (c *Car) take(calls map[int]bool, d Direction) []HallCall { delete(calls, c.Floor) c.Direction = d return []HallCall{{c.Floor, d}}}
// nextDirection is LOOK: keep going while there is work ahead, then turn around. Every stop// in the car's way is served on this sweep, and everything else on the next, so no call// waits forever.func (c *Car) nextDirection() Direction { above, below := c.HighestStop() > c.Floor, c.LowestStop() < c.Floor switch { case c.Direction == Up && above: return Up case c.Direction == Down && below: return Down case above: return Up case below: return Down } return Idle}
// HighestStop and LowestStop are the farthest stops each way; the current floor if none.func (c *Car) HighestStop() int { h := c.Floor for _, set := range []map[int]bool{c.carStops, c.upCalls, c.downCalls} { for f := range set { h = max(h, f) } } return h}
func (c *Car) LowestStop() int { l := c.Floor for _, set := range []map[int]bool{c.carStops, c.upCalls, c.downCalls} { for f := range set { l = min(l, f) } } return l}
func (c *Car) check(f int) error { if f < 0 || f >= c.floors { return fmt.Errorf("no floor %d", f) } return nil}// Which way a car is heading. 'idle' means it has nothing left to do.type Direction = 'up' | 'down' | 'idle';
// A button pressed on a floor. It is a promise to arrive *going that way*, which is// why it carries a direction and a button inside the car does not.interface HallCall { readonly floor: number; readonly direction: 'up' | 'down';}
class Car { floor = 0; direction: Direction = 'idle'; doorsOpen = false; // Three kinds of stop. A car stop is served whichever way the car is going; // an up call only by a car going up. private readonly carStops = new Set<number>(); private readonly upCalls = new Set<number>(); private readonly downCalls = new Set<number>();
constructor( readonly id: number, private readonly floors: number, // floors are 0 .. floors - 1 ) {}
carCall(f: number): void { this.check(f); this.carStops.add(f); }
hallCall(c: HallCall): void { this.check(c.floor); (c.direction === 'up' ? this.upCalls : this.downCalls).add(c.floor); }
// One tick: close the doors, or open them for the stops here, or move one floor. // Returns the hall calls answered this tick, so the building can turn their lights off. step(): HallCall[] { if (this.doorsOpen) { this.doorsOpen = false; return []; } const carStop = this.carStops.delete(this.floor); // before serveHere, which checks what is left above const served = this.serveHere(); if (carStop || served.length > 0) { this.doorsOpen = true; return served; } this.direction = this.nextDirection(); if (this.direction === 'up') this.floor++; else if (this.direction === 'down') this.floor--; return []; }
// The hall calls this floor can answer, given the way the car is heading. private serveHere(): HallCall[] { const f = this.floor; const d = this.direction; if (d !== 'down' && this.upCalls.has(f)) return this.take(this.upCalls, 'up'); if (d !== 'up' && this.downCalls.has(f)) return this.take(this.downCalls, 'down'); // The top of this sweep: nothing above, so the down call here is next anyway. if (d === 'up' && this.highestStop() <= f && this.downCalls.has(f)) return this.take(this.downCalls, 'down'); if (d === 'down' && this.lowestStop() >= f && this.upCalls.has(f)) return this.take(this.upCalls, 'up'); return []; }
private take(calls: Set<number>, d: 'up' | 'down'): HallCall[] { calls.delete(this.floor); this.direction = d; return [{ floor: this.floor, direction: d }]; }
// LOOK: keep going while there is work ahead, then turn around. Every stop in the car's // way is served on this sweep, and everything else on the next, so no call waits forever. private nextDirection(): Direction { const above = this.highestStop() > this.floor; const below = this.lowestStop() < this.floor; if (this.direction === 'up' && above) return 'up'; if (this.direction === 'down' && below) return 'down'; if (above) return 'up'; if (below) return 'down'; return 'idle'; }
// The farthest stops in each direction. With no stops, both are the current floor. highestStop(): number { return Math.max(this.floor, ...this.carStops, ...this.upCalls, ...this.downCalls); }
lowestStop(): number { return Math.min(this.floor, ...this.carStops, ...this.upCalls, ...this.downCalls); }
private check(f: number): void { if (!Number.isInteger(f) || f < 0 || f >= this.floors) throw new RangeError(`no floor ${f}`); }}Go deeperEverything one tick can do to a car1 min
Here is everything one tick can do to a car. Every tick is one arrow, so a test can drive the car along any path and check where it is after each step.
- Color key
- State
- Fails or stops
What does LOOK cost? It’s not the shortest total travel. A rider just behind the car waits for a full sweep, even if a quick turn would have served them. That’s the price of the promise that nobody waits forever, and it’s the right price.
Go deeperWhy does step() remove the car stop before checking floor calls?1 min
Because the turnaround check asks “is there anything above me?”. If the car stop for this floor is still in the set, the answer is “yes, this floor”, and a down call here gets skipped. The order of those two lines is exactly the kind of detail that the test “a down call at the top turns the car around” exists to protect.
Stage 3: Many cars and a dispatcher
Section titled “Stage 3: Many cars and a dispatcher”Now there are four elevators. Someone presses “up” on floor 11. Which car goes?
Whether you separate “which car” (a dispatcher, swappable) from “in what order” (the car’s sweep, unchanged), and whether your choice counts direction. “The closest car” is the answer they’ll push back on, with a car one floor away heading the other way.
Choosing a car is a different job from moving one. And the right rule depends on the building, and
even the time of day. So it becomes a Dispatcher interface, which is the Strategy pattern.
An ElevatorSystem owns the cars, and asks the dispatcher whenever a floor button is pressed. A
button inside a car needs no dispatcher: it goes straight to that car.
The first dispatcher sends the car that would arrive soonest, counted in floors of travel:
- An idle car costs its distance to the caller.
- A car already heading toward the caller, in the caller’s direction, costs its distance.
- Any other car must finish its sweep and come back. It costs the distance to where it turns around, plus the distance from there back to the caller.
So a car one floor away, but heading up to the top, costs the whole detour. An idle car five floors away wins. This is an estimate: it ignores the stops along the way. That’s fine for choosing between cars, and it’s the first thing a better dispatcher would improve.
- Color key
- Facade
- Entity
- Value object
- Enum
- Interface
- Implementation
- New in this step
// Picks which car answers a hall call. The rule changes from building to building// (and from morning to evening), so it is a strategy the system is given.interface Dispatcher { Car choose(List<Car> cars, HallCall call);}
// Sends the car that would arrive soonest, counted in floors travelled. A car already// heading toward the caller in the caller's direction is cheap; one that must finish its// sweep and come back pays for the whole detour.class NearestCarDispatcher implements Dispatcher { public Car choose(List<Car> cars, HallCall call) { // Ties go to the lower id, so the same situation always gets the same answer. return cars.stream() .min(Comparator.comparingInt((Car c) -> cost(c, call)).thenComparingInt(c -> c.id)) .orElseThrow(); }
static int cost(Car c, HallCall call) { int at = c.floor(), f = call.floor(); return switch (c.direction()) { case IDLE -> Math.abs(at - f); case UP -> { if (call.direction() == Direction.UP && f >= at) yield f - at; // on the way int turn = Math.max(c.highestStop(), f); // up to the top, then back yield (turn - at) + (turn - f); } case DOWN -> { if (call.direction() == Direction.DOWN && f <= at) yield at - f; int turn = Math.min(c.lowestStop(), f); yield (at - turn) + (f - turn); } }; }}
// The building's entry point. Buttons call it; it owns the cars and asks the dispatcher// which car answers each hall call. A button inside a car goes straight to that car.class ElevatorSystem { private final List<Car> cars = new ArrayList<>(); private final Dispatcher dispatcher;
ElevatorSystem(int floors, int carCount, Dispatcher dispatcher) { for (int i = 0; i < carCount; i++) cars.add(new Car(i, floors)); this.dispatcher = dispatcher; }
// The choice is made once, when the button is pressed. Stage 4 revisits that when // a car breaks down holding calls. Car hallCall(int floor, Direction d) { HallCall call = new HallCall(floor, d); Car car = dispatcher.choose(cars, call); car.hallCall(call); return car; }
void carCall(int carId, int floor) { cars.get(carId).carCall(floor); }
void step() { for (Car c : cars) c.step(); }
Car car(int id) { return cars.get(id); }}from typing import Protocol
# Picks which car answers a hall call. The rule changes from building to building# (and from morning to evening), so it is a strategy the system is given.class Dispatcher(Protocol): def choose(self, cars: list[Car], call: HallCall) -> Car: ...
# Sends the car that would arrive soonest, counted in floors travelled. A car already# heading toward the caller in the caller's direction is cheap; one that must finish its# sweep and come back pays for the whole detour.class NearestCarDispatcher: def choose(self, cars: list[Car], call: HallCall) -> Car: # Ties go to the lower id, so the same situation always gets the same answer. return min(cars, key=lambda c: (self.cost(c, call), c.id))
@staticmethod def cost(c: Car, call: HallCall) -> int: at, f = c.floor, call.floor if c.direction is Direction.IDLE: return abs(at - f) if c.direction is Direction.UP: if call.direction is Direction.UP and f >= at: return f - at # on the way turn = max(c.highest_stop(), f) # up to the top, then back return (turn - at) + (turn - f) if call.direction is Direction.DOWN and f <= at: return at - f turn = min(c.lowest_stop(), f) return (at - turn) + (f - turn)
# The building's entry point. Buttons call it; it owns the cars and asks the dispatcher# which car answers each hall call. A button inside a car goes straight to that car.class ElevatorSystem: def __init__(self, floors: int, car_count: int, dispatcher: Dispatcher): self._cars = [Car(i, floors) for i in range(car_count)] self._dispatcher = dispatcher
# The choice is made once, when the button is pressed. Stage 4 revisits that when # a car breaks down holding calls. def hall_call(self, floor: int, d: Direction) -> Car: call = HallCall(floor, d) car = self._dispatcher.choose(self._cars, call) car.hall_call(call) return car
def car_call(self, car_id: int, floor: int) -> None: self._cars[car_id].car_call(floor)
def step(self) -> None: for c in self._cars: c.step()
def car(self, id: int) -> Car: return self._cars[id]// Picks which car answers a hall call. The rule changes from building to building// (and from morning to evening), so it is a strategy the system is given.public interface IDispatcher{ Car Choose(IReadOnlyList<Car> cars, HallCall call);}
// Sends the car that would arrive soonest, counted in floors travelled. A car already// heading toward the caller in the caller's direction is cheap; one that must finish its// sweep and come back pays for the whole detour.public sealed class NearestCarDispatcher : IDispatcher{ // Ties go to the lower id, so the same situation always gets the same answer. public Car Choose(IReadOnlyList<Car> cars, HallCall call) => cars.OrderBy(c => Cost(c, call)).ThenBy(c => c.Id).First();
public static int Cost(Car c, HallCall call) { int at = c.Floor, f = call.Floor; switch (c.Direction) { case Direction.Up: { if (call.Direction == Direction.Up && f >= at) return f - at; // on the way int turn = Math.Max(c.HighestStop, f); // up to the top, then back return (turn - at) + (turn - f); } case Direction.Down: { if (call.Direction == Direction.Down && f <= at) return at - f; int turn = Math.Min(c.LowestStop, f); return (at - turn) + (f - turn); } default: return Math.Abs(at - f); } }}
// The building's entry point. Buttons call it; it owns the cars and asks the dispatcher// which car answers each hall call. A button inside a car goes straight to that car.public sealed class ElevatorSystem{ private readonly List<Car> _cars; private readonly IDispatcher _dispatcher;
public ElevatorSystem(int floors, int carCount, IDispatcher dispatcher) { _cars = Enumerable.Range(0, carCount).Select(i => new Car(i, floors)).ToList(); _dispatcher = dispatcher; }
// The choice is made once, when the button is pressed. Stage 4 revisits that when // a car breaks down holding calls. public Car HallCall(int floor, Direction d) { var call = new HallCall(floor, d); var car = _dispatcher.Choose(_cars, call); car.HallCall(call); return car; }
public void CarCall(int carId, int floor) => _cars[carId].CarCall(floor);
public void Step() { foreach (var c in _cars) c.Step(); }
public Car Car(int id) => _cars[id];}// Dispatcher picks which car answers a hall call. The rule changes from building to building// (and from morning to evening), so it is a strategy the system is given.type Dispatcher interface { Choose(cars []*Car, call HallCall) *Car}
// NearestCar sends the car that would arrive soonest, counted in floors travelled. A car// already heading toward the caller in the caller's direction is cheap; one that must finish// its sweep and come back pays for the whole detour.type NearestCar struct{}
func (NearestCar) Choose(cars []*Car, call HallCall) *Car { // Ties go to the lower id (the first in the slice), so the same situation always gets // the same answer. return slices.MinFunc(cars, func(a, b *Car) int { return cmp.Or(cmp.Compare(Cost(a, call), Cost(b, call)), cmp.Compare(a.ID, b.ID)) })}
func Cost(c *Car, call HallCall) int { at, f := c.Floor, call.Floor switch c.Direction { case Up: if call.Direction == Up && f >= at { return f - at // on the way } turn := max(c.HighestStop(), f) // up to the top, then back return (turn - at) + (turn - f) case Down: if call.Direction == Down && f <= at { return at - f } turn := min(c.LowestStop(), f) return (at - turn) + (f - turn) } if at > f { return at - f } return f - at}
// ElevatorSystem is the building's entry point. Buttons call it; it owns the cars and asks// the dispatcher which car answers each hall call. A button inside a car goes straight to that car.type ElevatorSystem struct { cars []*Car dispatcher Dispatcher}
func NewElevatorSystem(floors, carCount int, d Dispatcher) *ElevatorSystem { s := &ElevatorSystem{dispatcher: d} for i := range carCount { s.cars = append(s.cars, NewCar(i, floors)) } return s}
// HallCall makes the choice once, when the button is pressed. Stage 4 revisits that when// a car breaks down holding calls.func (s *ElevatorSystem) HallCall(floor int, d Direction) (*Car, error) { call := HallCall{floor, d} car := s.dispatcher.Choose(s.cars, call) return car, car.HallCall(call)}
func (s *ElevatorSystem) CarCall(carID, floor int) error { return s.cars[carID].CarCall(floor) }
func (s *ElevatorSystem) Step() { for _, c := range s.cars { c.Step() }}
func (s *ElevatorSystem) Car(id int) *Car { return s.cars[id] }// Picks which car answers a hall call. The rule changes from building to building// (and from morning to evening), so it is a strategy the system is given.interface Dispatcher { choose(cars: readonly Car[], call: HallCall): Car;}
// Sends the car that would arrive soonest, counted in floors travelled. A car already// heading toward the caller in the caller's direction is cheap; one that must finish its// sweep and come back pays for the whole detour.class NearestCarDispatcher implements Dispatcher { choose(cars: readonly Car[], call: HallCall): Car { // Ties go to the lower id, so the same situation always gets the same answer. return cars.reduce((best, c) => { const d = NearestCarDispatcher.cost(c, call) - NearestCarDispatcher.cost(best, call); return d < 0 || (d === 0 && c.id < best.id) ? c : best; }); }
static cost(c: Car, call: HallCall): number { const at = c.floor; const f = call.floor; if (c.direction === 'idle') return Math.abs(at - f); if (c.direction === 'up') { if (call.direction === 'up' && f >= at) return f - at; // on the way const turn = Math.max(c.highestStop(), f); // up to the top, then back return turn - at + (turn - f); } if (call.direction === 'down' && f <= at) return at - f; const turn = Math.min(c.lowestStop(), f); return at - turn + (f - turn); }}
// The building's entry point. Buttons call it; it owns the cars and asks the dispatcher// which car answers each hall call. A button inside a car goes straight to that car.class ElevatorSystem { private readonly cars: Car[];
constructor( floors: number, carCount: number, private readonly dispatcher: Dispatcher, ) { this.cars = Array.from({ length: carCount }, (_, i) => new Car(i, floors)); }
// The choice is made once, when the button is pressed. Stage 4 revisits that when // a car breaks down holding calls. hallCall(floor: number, direction: 'up' | 'down'): Car { const call = { floor, direction }; const car = this.dispatcher.choose(this.cars, call); car.hallCall(call); return car; }
carCall(carId: number, floor: number): void { this.cars[carId].carCall(floor); }
step(): void { for (const c of this.cars) c.step(); }
car(id: number): Car { return this.cars[id]; }}Go deeperOne call, from button to doors1 min
Here is one call from start to finish: the button is pressed, the system asks the dispatcher, the chosen car gets the call, and a later tick answers it and switches off the light.
- Color key
- Entity
- Interface
- Facade
The car is chosen once, when the button is pressed. That’s simple and predictable. Real buildings light a lantern over the chosen car’s doors right away, so riders walk to the right car, and that only works if the choice sticks. The cost: the choice can go stale. The chosen car might pick up five more stops and become slow. Stage 4 only reassigns a call when a car can’t serve it at all.
Go deeperWhy does the dispatcher read the cars, instead of each car working out its own cost?1 min
Because the cost is the dispatcher’s policy, not a fact about the car. A morning rush dispatcher
might ignore direction and prefer cars at the lobby. An energy-saving one might prefer a car that’s
already moving. If each car worked out its own cost, every policy change would edit Car. So the
car shares what it knows (its floor, its direction, its farthest stops), and the dispatcher
decides.
Stage 4: Cars that can’t take a call
Section titled “Stage 4: Cars that can’t take a call”Car 2 breaks down with three floor calls assigned to it. And people keep pressing the button that’s already lit. What happens?
Whether the building keeps a record of every accepted call and which car holds it, which makes reassigning and repeated presses easy. Answers that only fix the dispatcher (“skip broken cars”) leave those three riders waiting forever.
Three requirements arrive together, and one record answers all of them. The system now keeps a map from every accepted floor call to the car that holds it. That map is exactly what the lit buttons mean.
- A repeated press of a lit button finds the call already in the map, and does nothing.
- A car taken out of service hands back the floor calls it held. The system removes them from the map and sends each one to the dispatcher again.
- A full car keeps serving its riders, but the system stops offering it to the dispatcher. The calls it already holds stay with it, because some riders will get off before it gets there.
If no car can take a call (every car is full or broken), the call goes into a waiting queue. It’s offered again on every tick, oldest first. The dispatcher never sees broken or full cars: the system removes them from the list before asking, so no dispatcher has to remember to.
- Color key
- Facade
- Entity
- Value object
- Enum
- Interface
- Implementation
- New in this step
// One elevator car. It keeps its own stops and moves one tick at a time, sweeping up and down// (the LOOK rule below). A full car or one out of service gets no new hall calls.class Car { final int id; private final int floors; // floors are 0 .. floors - 1 private int floor; private Direction direction = Direction.IDLE; private boolean doorsOpen; // Three kinds of stop, each a sorted set so "anything above me?" is one lookup. // A car stop is served whichever way the car is going. An up call only by a car going up. private final TreeSet<Integer> carStops = new TreeSet<>(); private final TreeSet<Integer> upCalls = new TreeSet<>(); private final TreeSet<Integer> downCalls = new TreeSet<>(); private boolean inService = true; private boolean full; // from the load sensor
Car(int id, int floors) { this.id = id; this.floors = floors; }
// A full car still takes its riders where they asked, but shouldn't be sent for more. boolean accepting() { return inService && !full; }
void setFull(boolean full) { this.full = full; }
// Stop here with the doors open, and hand back the hall calls this car was holding so // the building can give them to another car. Without this, those people wait forever. List<HallCall> takeOutOfService() { inService = false; doorsOpen = true; direction = Direction.IDLE; List<HallCall> orphans = new ArrayList<>(); for (int f : upCalls) orphans.add(new HallCall(f, Direction.UP)); for (int f : downCalls) orphans.add(new HallCall(f, Direction.DOWN)); upCalls.clear(); downCalls.clear(); carStops.clear(); // riders step out here; their buttons no longer mean anything return orphans; }
void returnToService() { inService = true; } // the doors close on the next tick
// A button inside the car: stop at f whichever way the car is going. void carCall(int f) { check(f); carStops.add(f); }
// A hall call the building gave this car: stop at that floor going that way. void hallCall(HallCall c) { check(c.floor()); (c.direction() == Direction.UP ? upCalls : downCalls).add(c.floor()); }
// One tick: close the doors, or open them for the stops here, or move one floor. // Returns the hall calls answered this tick, so the building can turn their lights off. List<HallCall> step() { if (!inService) return List.of(); if (doorsOpen) { doorsOpen = false; return List.of(); } boolean carStop = carStops.remove(floor); // before serveHere, which checks what is left above List<HallCall> served = serveHere(); if (carStop || !served.isEmpty()) { doorsOpen = true; return served; } direction = nextDirection(); if (direction == Direction.UP) floor++; else if (direction == Direction.DOWN) floor--; return List.of(); }
// The hall calls this floor can answer, given the way the car is heading. private List<HallCall> serveHere() { List<HallCall> served = new ArrayList<>(); if (direction != Direction.DOWN && upCalls.remove(floor)) { served.add(new HallCall(floor, Direction.UP)); direction = Direction.UP; } else if (direction != Direction.UP && downCalls.remove(floor)) { served.add(new HallCall(floor, Direction.DOWN)); direction = Direction.DOWN; } else if (direction == Direction.UP && highestStop() <= floor && downCalls.remove(floor)) { // The top of this sweep: nothing above, so the down call here is next anyway. served.add(new HallCall(floor, Direction.DOWN)); direction = Direction.DOWN; } else if (direction == Direction.DOWN && lowestStop() >= floor && upCalls.remove(floor)) { served.add(new HallCall(floor, Direction.UP)); direction = Direction.UP; } return served; }
// LOOK: keep going while there is work ahead, then turn around. Every stop in the car's // way is served on this sweep, and everything else on the next, so no call waits forever. private Direction nextDirection() { boolean above = highestStop() > floor, below = lowestStop() < floor; if (direction == Direction.UP && above) return Direction.UP; if (direction == Direction.DOWN && below) return Direction.DOWN; if (above) return Direction.UP; if (below) return Direction.DOWN; return Direction.IDLE; }
// The farthest stops in each direction. With no stops, both are the current floor. int highestStop() { return max(max(max(floor, carStops), upCalls), downCalls); } int lowestStop() { return min(min(min(floor, carStops), upCalls), downCalls); }
private static int max(int f, TreeSet<Integer> s) { return s.isEmpty() ? f : Math.max(f, s.last()); } private static int min(int f, TreeSet<Integer> s) { return s.isEmpty() ? f : Math.min(f, s.first()); }
private void check(int f) { if (f < 0 || f >= floors) throw new IllegalArgumentException("no floor " + f); }
int floor() { return floor; } Direction direction() { return direction; } boolean doorsOpen() { return doorsOpen; }}
// The building's entry point. Buttons call it; it owns the cars and asks the dispatcher which car// answers each hall call. It also remembers every call it accepted and which car holds it: that// map makes a second press harmless and lets a broken car's calls move.class ElevatorSystem { private final List<Car> cars = new ArrayList<>(); private final Dispatcher dispatcher; private final Map<HallCall, Car> assigned = new HashMap<>(); // lit buttons -> who answers them private final Deque<HallCall> waiting = new ArrayDeque<>(); // accepted, but no car can take them yet
ElevatorSystem(int floors, int carCount, Dispatcher dispatcher) { for (int i = 0; i < carCount; i++) cars.add(new Car(i, floors)); this.dispatcher = dispatcher; }
void hallCall(int floor, Direction d) { HallCall call = new HallCall(floor, d); // People press a lit button again and again. Only the first press is a new call. if (assigned.containsKey(call) || waiting.contains(call)) return; if (!assign(call)) waiting.addLast(call); }
void carCall(int carId, int floor) { cars.get(carId).carCall(floor); }
// The car's calls go to other cars, or wait if none can take them. void takeOutOfService(int carId) { for (HallCall orphan : cars.get(carId).takeOutOfService()) { assigned.remove(orphan); if (!assign(orphan)) waiting.addLast(orphan); } }
void returnToService(int carId) { cars.get(carId).returnToService(); }
void setFull(int carId, boolean full) { cars.get(carId).setFull(full); }
void step() { // Calls that found no car get another chance every tick, oldest first. for (int i = waiting.size(); i > 0; i--) { HallCall c = waiting.removeFirst(); if (!assign(c)) waiting.addLast(c); } for (Car car : cars) for (HallCall served : car.step()) assigned.remove(served); }
// Dispatchers only ever see cars that can take the call, so no dispatcher has to // remember to skip broken or full cars. private boolean assign(HallCall call) { List<Car> candidates = cars.stream().filter(Car::accepting).toList(); if (candidates.isEmpty()) return false; Car car = dispatcher.choose(candidates, call); car.hallCall(call); assigned.put(call, car); return true; }
boolean isLit(HallCall call) { return assigned.containsKey(call) || waiting.contains(call); }
Car car(int id) { return cars.get(id); }}from collections import deque
# One elevator car. It keeps its own stops and moves one tick at a time, sweeping up and down# (the LOOK rule below). A full car or one out of service gets no new hall calls.class Car: def __init__(self, id: int, floors: int): self.id = id self._floors = floors # floors are 0 .. floors - 1 self.floor = 0 self.direction = Direction.IDLE self.doors_open = False # Three kinds of stop. A car stop is served whichever way the car is going; # an up call only by a car going up. Plain sets: max() and min() over a handful of # floors is cheap, and a sorted container would add a dependency for nothing. self._car_stops: set[int] = set() self._up_calls: set[int] = set() self._down_calls: set[int] = set() self._in_service = True self.full = False # from the load sensor
# A full car still takes its riders where they asked, but shouldn't be sent for more. def accepting(self) -> bool: return self._in_service and not self.full
# Stop here with the doors open, and hand back the hall calls this car was holding so # the building can give them to another car. Without this, those people wait forever. def take_out_of_service(self) -> list[HallCall]: self._in_service = False self.doors_open = True self.direction = Direction.IDLE orphans = [HallCall(f, Direction.UP) for f in sorted(self._up_calls)] orphans += [HallCall(f, Direction.DOWN) for f in sorted(self._down_calls)] self._up_calls.clear() self._down_calls.clear() self._car_stops.clear() # riders step out here; their buttons no longer mean anything return orphans
def return_to_service(self) -> None: self._in_service = True # the doors close on the next tick
# A button inside the car: stop at f whichever way the car is going. def car_call(self, f: int) -> None: self._check(f) self._car_stops.add(f)
# A hall call the building gave this car: stop at that floor going that way. def hall_call(self, c: HallCall) -> None: self._check(c.floor) (self._up_calls if c.direction is Direction.UP else self._down_calls).add(c.floor)
# One tick: close the doors, or open them for the stops here, or move one floor. # Returns the hall calls answered this tick, so the building can turn their lights off. def step(self) -> list[HallCall]: if not self._in_service: return [] if self.doors_open: self.doors_open = False return [] car_stop = self.floor in self._car_stops # before _serve_here, which checks what is left above self._car_stops.discard(self.floor) served = self._serve_here() if car_stop or served: self.doors_open = True return served self.direction = self._next_direction() if self.direction is Direction.UP: self.floor += 1 elif self.direction is Direction.DOWN: self.floor -= 1 return []
# The hall calls this floor can answer, given the way the car is heading. def _serve_here(self) -> list[HallCall]: f, d = self.floor, self.direction if d is not Direction.DOWN and f in self._up_calls: return self._take(self._up_calls, Direction.UP) if d is not Direction.UP and f in self._down_calls: return self._take(self._down_calls, Direction.DOWN) # The top of this sweep: nothing above, so the down call here is next anyway. if d is Direction.UP and self.highest_stop() <= f and f in self._down_calls: return self._take(self._down_calls, Direction.DOWN) if d is Direction.DOWN and self.lowest_stop() >= f and f in self._up_calls: return self._take(self._up_calls, Direction.UP) return []
def _take(self, calls: set[int], d: Direction) -> list[HallCall]: calls.discard(self.floor) self.direction = d return [HallCall(self.floor, d)]
# LOOK: keep going while there is work ahead, then turn around. Every stop in the car's # way is served on this sweep, and everything else on the next, so no call waits forever. def _next_direction(self) -> Direction: above, below = self.highest_stop() > self.floor, self.lowest_stop() < self.floor if self.direction is Direction.UP and above: return Direction.UP if self.direction is Direction.DOWN and below: return Direction.DOWN if above: return Direction.UP if below: return Direction.DOWN return Direction.IDLE
# The farthest stops in each direction. With no stops, both are the current floor. def highest_stop(self) -> int: return max([self.floor, *self._car_stops, *self._up_calls, *self._down_calls])
def lowest_stop(self) -> int: return min([self.floor, *self._car_stops, *self._up_calls, *self._down_calls])
def _check(self, f: int) -> None: if not 0 <= f < self._floors: raise ValueError(f"no floor {f}")
# The building's entry point. Buttons call it; it owns the cars and asks the dispatcher which# car answers each hall call. It also remembers every call it accepted and which car holds it:# that map makes a second press harmless and lets a broken car's calls move.class ElevatorSystem: def __init__(self, floors: int, car_count: int, dispatcher: Dispatcher): self._cars = [Car(i, floors) for i in range(car_count)] self._dispatcher = dispatcher self._assigned: dict[HallCall, Car] = {} # lit buttons -> who answers them self._waiting: deque[HallCall] = deque() # accepted, but no car can take them yet
def hall_call(self, floor: int, d: Direction) -> None: call = HallCall(floor, d) # People press a lit button again and again. Only the first press is a new call. if self.is_lit(call): return if not self._assign(call): self._waiting.append(call)
def car_call(self, car_id: int, floor: int) -> None: self._cars[car_id].car_call(floor)
# The car's calls go to other cars, or wait if none can take them. def take_out_of_service(self, car_id: int) -> None: for orphan in self._cars[car_id].take_out_of_service(): self._assigned.pop(orphan, None) if not self._assign(orphan): self._waiting.append(orphan)
def return_to_service(self, car_id: int) -> None: self._cars[car_id].return_to_service()
def set_full(self, car_id: int, full: bool) -> None: self._cars[car_id].full = full
def step(self) -> None: # Calls that found no car get another chance every tick, oldest first. for _ in range(len(self._waiting)): c = self._waiting.popleft() if not self._assign(c): self._waiting.append(c) for car in self._cars: for served in car.step(): self._assigned.pop(served, None)
# Dispatchers only ever see cars that can take the call, so no dispatcher has to # remember to skip broken or full cars. def _assign(self, call: HallCall) -> bool: candidates = [c for c in self._cars if c.accepting()] if not candidates: return False car = self._dispatcher.choose(candidates, call) car.hall_call(call) self._assigned[call] = car return True
def is_lit(self, call: HallCall) -> bool: return call in self._assigned or call in self._waiting
def car(self, id: int) -> Car: return self._cars[id]// One elevator car. It keeps its own stops and moves one tick at a time, sweeping up and down// (the LOOK rule below). A full car or one out of service gets no new hall calls.public sealed class Car(int id, int floors) // floors are 0 .. floors - 1{ public int Id { get; } = id; public int Floor { get; private set; } public Direction Direction { get; private set; } = Direction.Idle; public bool DoorsOpen { get; private set; }
// Three kinds of stop, each a sorted set so "anything above me?" is one lookup. // A car stop is served whichever way the car is going. An up call only by a car going up. private readonly SortedSet<int> _carStops = []; private readonly SortedSet<int> _upCalls = []; private readonly SortedSet<int> _downCalls = []; private bool _inService = true;
public bool Full { get; set; } // from the load sensor
// A full car still takes its riders where they asked, but shouldn't be sent for more. public bool Accepting => _inService && !Full;
// Stop here with the doors open, and hand back the hall calls this car was holding so // the building can give them to another car. Without this, those people wait forever. public List<HallCall> TakeOutOfService() { _inService = false; DoorsOpen = true; Direction = Direction.Idle; var orphans = _upCalls.Select(f => new HallCall(f, Direction.Up)) .Concat(_downCalls.Select(f => new HallCall(f, Direction.Down))) .ToList(); _upCalls.Clear(); _downCalls.Clear(); _carStops.Clear(); // riders step out here; their buttons no longer mean anything return orphans; }
public void ReturnToService() => _inService = true; // the doors close on the next tick
// A button inside the car: stop at f whichever way the car is going. public void CarCall(int f) { Check(f); _carStops.Add(f); }
// A hall call the building gave this car: stop at that floor going that way. public void HallCall(HallCall c) { Check(c.Floor); (c.Direction == Direction.Up ? _upCalls : _downCalls).Add(c.Floor); }
// One tick: close the doors, or open them for the stops here, or move one floor. // Returns the hall calls answered this tick, so the building can turn their lights off. public IReadOnlyList<HallCall> Step() { if (!_inService) return []; if (DoorsOpen) { DoorsOpen = false; return []; } bool carStop = _carStops.Remove(Floor); // before ServeHere, which checks what is left above var served = ServeHere(); if (carStop || served.Count > 0) { DoorsOpen = true; return served; } Direction = NextDirection(); if (Direction == Direction.Up) Floor++; else if (Direction == Direction.Down) Floor--; return []; }
// The hall calls this floor can answer, given the way the car is heading. private List<HallCall> ServeHere() { if (Direction != Direction.Down && _upCalls.Contains(Floor)) return Take(_upCalls, Direction.Up); if (Direction != Direction.Up && _downCalls.Contains(Floor)) return Take(_downCalls, Direction.Down); // The top of this sweep: nothing above, so the down call here is next anyway. if (Direction == Direction.Up && HighestStop <= Floor && _downCalls.Contains(Floor)) return Take(_downCalls, Direction.Down); if (Direction == Direction.Down && LowestStop >= Floor && _upCalls.Contains(Floor)) return Take(_upCalls, Direction.Up); return []; }
private List<HallCall> Take(SortedSet<int> calls, Direction d) { calls.Remove(Floor); Direction = d; return [new HallCall(Floor, d)]; }
// LOOK: keep going while there is work ahead, then turn around. Every stop in the car's // way is served on this sweep, and everything else on the next, so no call waits forever. private Direction NextDirection() { bool above = HighestStop > Floor, below = LowestStop < Floor; if (Direction == Direction.Up && above) return Direction.Up; if (Direction == Direction.Down && below) return Direction.Down; if (above) return Direction.Up; if (below) return Direction.Down; return Direction.Idle; }
// The farthest stops in each direction. With no stops, both are the current floor. public int HighestStop => Math.Max(Floor, Math.Max(Max(_carStops), Math.Max(Max(_upCalls), Max(_downCalls)))); public int LowestStop => Math.Min(Floor, Math.Min(Min(_carStops), Math.Min(Min(_upCalls), Min(_downCalls))));
private static int Max(SortedSet<int> s) => s.Count == 0 ? int.MinValue : s.Max; private static int Min(SortedSet<int> s) => s.Count == 0 ? int.MaxValue : s.Min;
private void Check(int f) { if (f < 0 || f >= floors) throw new ArgumentOutOfRangeException(nameof(f), $"no floor {f}"); }}
// The building's entry point. Buttons call it; it owns the cars and asks the dispatcher which// car answers each hall call. It also remembers every call it accepted and which car holds it:// that map makes a second press harmless and lets a broken car's calls move.public sealed class ElevatorSystem{ private readonly List<Car> _cars; private readonly IDispatcher _dispatcher; private readonly Dictionary<HallCall, Car> _assigned = new(); // lit buttons -> who answers them private readonly LinkedList<HallCall> _waiting = new(); // accepted, but no car can take them yet
public ElevatorSystem(int floors, int carCount, IDispatcher dispatcher) { _cars = Enumerable.Range(0, carCount).Select(i => new Car(i, floors)).ToList(); _dispatcher = dispatcher; }
public void HallCall(int floor, Direction d) { var call = new HallCall(floor, d); // People press a lit button again and again. Only the first press is a new call. if (IsLit(call)) return; if (!Assign(call)) _waiting.AddLast(call); }
public void CarCall(int carId, int floor) => _cars[carId].CarCall(floor);
// The car's calls go to other cars, or wait if none can take them. public void TakeOutOfService(int carId) { foreach (var orphan in _cars[carId].TakeOutOfService()) { _assigned.Remove(orphan); if (!Assign(orphan)) _waiting.AddLast(orphan); } }
public void ReturnToService(int carId) => _cars[carId].ReturnToService();
public void SetFull(int carId, bool full) => _cars[carId].Full = full;
public void Step() { // Calls that found no car get another chance every tick, oldest first. for (int i = _waiting.Count; i > 0; i--) { var c = _waiting.First!.Value; _waiting.RemoveFirst(); if (!Assign(c)) _waiting.AddLast(c); } foreach (var car in _cars) foreach (var served in car.Step()) _assigned.Remove(served); }
// Dispatchers only ever see cars that can take the call, so no dispatcher has to // remember to skip broken or full cars. private bool Assign(HallCall call) { var candidates = _cars.Where(c => c.Accepting).ToList(); if (candidates.Count == 0) return false; var car = _dispatcher.Choose(candidates, call); car.HallCall(call); _assigned[call] = car; return true; }
public bool IsLit(HallCall call) => _assigned.ContainsKey(call) || _waiting.Contains(call);
public Car Car(int id) => _cars[id];}// One elevator car. It keeps its own stops and moves one tick at a time, sweeping up and down// (the LOOK rule below). A full car or one out of service gets no new hall calls.type Car struct { ID int floors int // floors are 0 .. floors-1 Floor int Direction Direction DoorsOpen bool // Three kinds of stop. A car stop is served whichever way the car is going; // an up call only by a car going up. Maps as sets: a building has tens of floors, // so scanning them for the highest stop costs nothing. carStops, upCalls, downCalls map[int]bool inService bool Full bool // from the load sensor}
func NewCar(id, floors int) *Car { return &Car{ID: id, floors: floors, inService: true, carStops: map[int]bool{}, upCalls: map[int]bool{}, downCalls: map[int]bool{}}}
// Accepting: a full car still takes its riders where they asked, but shouldn't be sent for more.func (c *Car) Accepting() bool { return c.inService && !c.Full }
// TakeOutOfService stops the car here with the doors open, and hands back the hall calls it// was holding so the building can give them to another car. Without this, those people// wait forever.func (c *Car) TakeOutOfService() []HallCall { c.inService, c.DoorsOpen, c.Direction = false, true, Idle var orphans []HallCall for _, f := range slices.Sorted(maps.Keys(c.upCalls)) { orphans = append(orphans, HallCall{f, Up}) } for _, f := range slices.Sorted(maps.Keys(c.downCalls)) { orphans = append(orphans, HallCall{f, Down}) } clear(c.upCalls) clear(c.downCalls) clear(c.carStops) // riders step out here; their buttons no longer mean anything return orphans}
func (c *Car) ReturnToService() { c.inService = true } // the doors close on the next tick
// CarCall is a button inside the car: stop at f whichever way the car is going.func (c *Car) CarCall(f int) error { if err := c.check(f); err != nil { return err } c.carStops[f] = true return nil}
// HallCall is a call the building gave this car: stop at that floor going that way.func (c *Car) HallCall(h HallCall) error { if err := c.check(h.Floor); err != nil { return err } if h.Direction == Up { c.upCalls[h.Floor] = true } else { c.downCalls[h.Floor] = true } return nil}
// Step does one thing: close the doors, or open them for the stops here, or move one floor.// It returns the hall calls answered this tick, so the building can turn their lights off.func (c *Car) Step() []HallCall { if !c.inService { return nil } if c.DoorsOpen { c.DoorsOpen = false return nil } carStop := c.carStops[c.Floor] // before serveHere, which checks what is left above delete(c.carStops, c.Floor) served := c.serveHere() if carStop || len(served) > 0 { c.DoorsOpen = true return served } c.Direction = c.nextDirection() switch c.Direction { case Up: c.Floor++ case Down: c.Floor-- } return nil}
// serveHere answers the hall calls this floor can take, given the way the car is heading.func (c *Car) serveHere() []HallCall { f, d := c.Floor, c.Direction switch { case d != Down && c.upCalls[f]: return c.take(c.upCalls, Up) case d != Up && c.downCalls[f]: return c.take(c.downCalls, Down) case d == Up && c.HighestStop() <= f && c.downCalls[f]: // The top of this sweep: nothing above, so the down call here is next anyway. return c.take(c.downCalls, Down) case d == Down && c.LowestStop() >= f && c.upCalls[f]: return c.take(c.upCalls, Up) } return nil}
func (c *Car) take(calls map[int]bool, d Direction) []HallCall { delete(calls, c.Floor) c.Direction = d return []HallCall{{c.Floor, d}}}
// nextDirection is LOOK: keep going while there is work ahead, then turn around. Every stop// in the car's way is served on this sweep, and everything else on the next, so no call// waits forever.func (c *Car) nextDirection() Direction { above, below := c.HighestStop() > c.Floor, c.LowestStop() < c.Floor switch { case c.Direction == Up && above: return Up case c.Direction == Down && below: return Down case above: return Up case below: return Down } return Idle}
// HighestStop and LowestStop are the farthest stops each way; the current floor if none.func (c *Car) HighestStop() int { h := c.Floor for _, set := range []map[int]bool{c.carStops, c.upCalls, c.downCalls} { for f := range set { h = max(h, f) } } return h}
func (c *Car) LowestStop() int { l := c.Floor for _, set := range []map[int]bool{c.carStops, c.upCalls, c.downCalls} { for f := range set { l = min(l, f) } } return l}
func (c *Car) check(f int) error { if f < 0 || f >= c.floors { return fmt.Errorf("no floor %d", f) } return nil}
// ElevatorSystem is the building's entry point. Buttons call it; it owns the cars and asks the dispatcher which// car answers each hall call. It also remembers every call it accepted and which car holds it:// that map makes a second press harmless and lets a broken car's calls move.type ElevatorSystem struct { cars []*Car dispatcher Dispatcher assigned map[HallCall]*Car // lit buttons -> who answers them waiting []HallCall // accepted, but no car can take them yet}
func NewElevatorSystem(floors, carCount int, d Dispatcher) *ElevatorSystem { s := &ElevatorSystem{dispatcher: d, assigned: map[HallCall]*Car{}} for i := range carCount { s.cars = append(s.cars, NewCar(i, floors)) } return s}
func (s *ElevatorSystem) HallCall(floor int, d Direction) error { call := HallCall{floor, d} // People press a lit button again and again. Only the first press is a new call. if s.IsLit(call) { return nil } ok, err := s.assign(call) if err == nil && !ok { s.waiting = append(s.waiting, call) } return err}
func (s *ElevatorSystem) CarCall(carID, floor int) error { return s.cars[carID].CarCall(floor) }
// TakeOutOfService gives the car's calls to other cars, or makes them wait if none can take them.func (s *ElevatorSystem) TakeOutOfService(carID int) { for _, orphan := range s.cars[carID].TakeOutOfService() { delete(s.assigned, orphan) if ok, _ := s.assign(orphan); !ok { s.waiting = append(s.waiting, orphan) } }}
func (s *ElevatorSystem) ReturnToService(carID int) { s.cars[carID].ReturnToService() }
func (s *ElevatorSystem) SetFull(carID int, full bool) { s.cars[carID].Full = full }
func (s *ElevatorSystem) Step() { // Calls that found no car get another chance every tick, oldest first. still := s.waiting[:0] for _, c := range s.waiting { if ok, _ := s.assign(c); !ok { still = append(still, c) } } s.waiting = still for _, car := range s.cars { for _, served := range car.Step() { delete(s.assigned, served) } }}
// assign shows dispatchers only the cars that can take the call, so no dispatcher has to// remember to skip broken or full cars.func (s *ElevatorSystem) assign(call HallCall) (bool, error) { var candidates []*Car for _, c := range s.cars { if c.Accepting() { candidates = append(candidates, c) } } if len(candidates) == 0 { return false, nil } car := s.dispatcher.Choose(candidates, call) if err := car.HallCall(call); err != nil { return false, err } s.assigned[call] = car return true, nil}
func (s *ElevatorSystem) IsLit(call HallCall) bool { _, ok := s.assigned[call] return ok || slices.Contains(s.waiting, call)}
func (s *ElevatorSystem) Car(id int) *Car { return s.cars[id] }// One elevator car. It keeps its own stops and moves one tick at a time, sweeping up and down// (the LOOK rule below). A full car or one out of service gets no new hall calls.class Car { floor = 0; direction: Direction = 'idle'; doorsOpen = false; // Three kinds of stop. A car stop is served whichever way the car is going; // an up call only by a car going up. private readonly carStops = new Set<number>(); private readonly upCalls = new Set<number>(); private readonly downCalls = new Set<number>(); private inService = true; full = false; // from the load sensor
constructor( readonly id: number, private readonly floors: number, // floors are 0 .. floors - 1 ) {}
// A full car still takes its riders where they asked, but shouldn't be sent for more. accepting(): boolean { return this.inService && !this.full; }
// Stop here with the doors open, and hand back the hall calls this car was holding so // the building can give them to another car. Without this, those people wait forever. takeOutOfService(): HallCall[] { this.inService = false; this.doorsOpen = true; this.direction = 'idle'; const sorted = (s: Set<number>) => [...s].sort((a, b) => a - b); const orphans: HallCall[] = [ ...sorted(this.upCalls).map((floor) => ({ floor, direction: 'up' as const })), ...sorted(this.downCalls).map((floor) => ({ floor, direction: 'down' as const })), ]; this.upCalls.clear(); this.downCalls.clear(); this.carStops.clear(); // riders step out here; their buttons no longer mean anything return orphans; }
returnToService(): void { this.inService = true; // the doors close on the next tick }
// A button inside the car: stop at f whichever way the car is going. carCall(f: number): void { this.check(f); this.carStops.add(f); }
// A hall call the building gave this car: stop at that floor going that way. hallCall(c: HallCall): void { this.check(c.floor); (c.direction === 'up' ? this.upCalls : this.downCalls).add(c.floor); }
// One tick: close the doors, or open them for the stops here, or move one floor. // Returns the hall calls answered this tick, so the building can turn their lights off. step(): HallCall[] { if (!this.inService) return []; if (this.doorsOpen) { this.doorsOpen = false; return []; } const carStop = this.carStops.delete(this.floor); // before serveHere, which checks what is left above const served = this.serveHere(); if (carStop || served.length > 0) { this.doorsOpen = true; return served; } this.direction = this.nextDirection(); if (this.direction === 'up') this.floor++; else if (this.direction === 'down') this.floor--; return []; }
// The hall calls this floor can answer, given the way the car is heading. private serveHere(): HallCall[] { const f = this.floor; const d = this.direction; if (d !== 'down' && this.upCalls.has(f)) return this.take(this.upCalls, 'up'); if (d !== 'up' && this.downCalls.has(f)) return this.take(this.downCalls, 'down'); // The top of this sweep: nothing above, so the down call here is next anyway. if (d === 'up' && this.highestStop() <= f && this.downCalls.has(f)) return this.take(this.downCalls, 'down'); if (d === 'down' && this.lowestStop() >= f && this.upCalls.has(f)) return this.take(this.upCalls, 'up'); return []; }
private take(calls: Set<number>, d: 'up' | 'down'): HallCall[] { calls.delete(this.floor); this.direction = d; return [{ floor: this.floor, direction: d }]; }
// LOOK: keep going while there is work ahead, then turn around. Every stop in the car's // way is served on this sweep, and everything else on the next, so no call waits forever. private nextDirection(): Direction { const above = this.highestStop() > this.floor; const below = this.lowestStop() < this.floor; if (this.direction === 'up' && above) return 'up'; if (this.direction === 'down' && below) return 'down'; if (above) return 'up'; if (below) return 'down'; return 'idle'; }
// The farthest stops in each direction. With no stops, both are the current floor. highestStop(): number { return Math.max(this.floor, ...this.carStops, ...this.upCalls, ...this.downCalls); }
lowestStop(): number { return Math.min(this.floor, ...this.carStops, ...this.upCalls, ...this.downCalls); }
private check(f: number): void { if (!Number.isInteger(f) || f < 0 || f >= this.floors) throw new RangeError(`no floor ${f}`); }}
// A hall call as a map key. Objects compare by reference in JavaScript, so two presses of// the same button need a string that is equal whenever the calls are.const keyOf = (c: HallCall) => `${c.floor}:${c.direction}`;
// The building's entry point. Buttons call it; it owns the cars and asks the dispatcher which// car answers each hall call. It also remembers every call it accepted and which car holds it:// that map makes a second press harmless and lets a broken car's calls move.class ElevatorSystem { private readonly cars: Car[]; private readonly assigned = new Map<string, Car>(); // lit buttons -> who answers them private waiting: HallCall[] = []; // accepted, but no car can take them yet
constructor( floors: number, carCount: number, private readonly dispatcher: Dispatcher, ) { this.cars = Array.from({ length: carCount }, (_, i) => new Car(i, floors)); }
hallCall(floor: number, direction: 'up' | 'down'): void { const call = { floor, direction }; // People press a lit button again and again. Only the first press is a new call. if (this.isLit(call)) return; if (!this.assign(call)) this.waiting.push(call); }
carCall(carId: number, floor: number): void { this.cars[carId].carCall(floor); }
// The car's calls go to other cars, or wait if none can take them. takeOutOfService(carId: number): void { for (const orphan of this.cars[carId].takeOutOfService()) { this.assigned.delete(keyOf(orphan)); if (!this.assign(orphan)) this.waiting.push(orphan); } }
returnToService(carId: number): void { this.cars[carId].returnToService(); }
setFull(carId: number, full: boolean): void { this.cars[carId].full = full; }
step(): void { // Calls that found no car get another chance every tick, oldest first. this.waiting = this.waiting.filter((c) => !this.assign(c)); for (const car of this.cars) for (const served of car.step()) this.assigned.delete(keyOf(served)); }
// Dispatchers only ever see cars that can take the call, so no dispatcher has to // remember to skip broken or full cars. private assign(call: HallCall): boolean { const candidates = this.cars.filter((c) => c.accepting()); if (candidates.length === 0) return false; const car = this.dispatcher.choose(candidates, call); car.hallCall(call); this.assigned.set(keyOf(call), car); return true; }
isLit(call: HallCall): boolean { return this.assigned.has(keyOf(call)) || this.waiting.some((w) => keyOf(w) === keyOf(call)); }
car(id: number): Car { return this.cars[id]; }}“Out of service” and “full” are two simple flags on the car, plus one early return in step(). The
sweep logic stays in one place.
Go deeperFlags now, the State pattern later1 min
Real buildings have more modes: fire service (send every car to the lobby, ignore floor calls), a porter driving the car, VIP. Each one changes several behaviors at once. With three or more of those, a separate object per mode (the State pattern) beats a growing pile of flags.
Go deeperWhat happens to riders inside a car taken out of service?1 min
In our model, the car stops at the floor it’s on and opens its doors. Its car stops are cleared, because those riders step out. A real car would first finish moving to the nearest floor. If riders must be taken to their floors first, “out of service” becomes “stop accepting floor calls, finish the car stops, then park”. That’s one more mode, and a good reason to switch to the State pattern.
This is enough for a mid-level answer. A car sweeps with floor calls that carry a direction, and a dispatcher chooses the car with a cost that counts direction. The rest of the page is what makes it a senior answer.
The full design
Section titled “The full design”Here is everything together. Each class is here because a requirement needed it. Controller
comes from the next section.
- Color key
- Facade
- Entity
- Value object
- Enum
- Interface
- Implementation
// The complete design, built from every stage on this page.
import java.util.*;import java.util.concurrent.*;
// One elevator car. It keeps its own stops and moves one tick at a time, sweeping up and down// (the LOOK rule below). A full car or one out of service gets no new hall calls.class Car { final int id; private final int floors; // floors are 0 .. floors - 1 private int floor; private Direction direction = Direction.IDLE; private boolean doorsOpen; // Three kinds of stop, each a sorted set so "anything above me?" is one lookup. // A car stop is served whichever way the car is going. An up call only by a car going up. private final TreeSet<Integer> carStops = new TreeSet<>(); private final TreeSet<Integer> upCalls = new TreeSet<>(); private final TreeSet<Integer> downCalls = new TreeSet<>(); private boolean inService = true; private boolean full; // from the load sensor
Car(int id, int floors) { this.id = id; this.floors = floors; }
// A full car still takes its riders where they asked, but shouldn't be sent for more. boolean accepting() { return inService && !full; }
void setFull(boolean full) { this.full = full; }
// Stop here with the doors open, and hand back the hall calls this car was holding so // the building can give them to another car. Without this, those people wait forever. List<HallCall> takeOutOfService() { inService = false; doorsOpen = true; direction = Direction.IDLE; List<HallCall> orphans = new ArrayList<>(); for (int f : upCalls) orphans.add(new HallCall(f, Direction.UP)); for (int f : downCalls) orphans.add(new HallCall(f, Direction.DOWN)); upCalls.clear(); downCalls.clear(); carStops.clear(); // riders step out here; their buttons no longer mean anything return orphans; }
void returnToService() { inService = true; } // the doors close on the next tick
// A button inside the car: stop at f whichever way the car is going. void carCall(int f) { check(f); carStops.add(f); }
// A hall call the building gave this car: stop at that floor going that way. void hallCall(HallCall c) { check(c.floor()); (c.direction() == Direction.UP ? upCalls : downCalls).add(c.floor()); }
// One tick: close the doors, or open them for the stops here, or move one floor. // Returns the hall calls answered this tick, so the building can turn their lights off. List<HallCall> step() { if (!inService) return List.of(); if (doorsOpen) { doorsOpen = false; return List.of(); } boolean carStop = carStops.remove(floor); // before serveHere, which checks what is left above List<HallCall> served = serveHere(); if (carStop || !served.isEmpty()) { doorsOpen = true; return served; } direction = nextDirection(); if (direction == Direction.UP) floor++; else if (direction == Direction.DOWN) floor--; return List.of(); }
// The hall calls this floor can answer, given the way the car is heading. private List<HallCall> serveHere() { List<HallCall> served = new ArrayList<>(); if (direction != Direction.DOWN && upCalls.remove(floor)) { served.add(new HallCall(floor, Direction.UP)); direction = Direction.UP; } else if (direction != Direction.UP && downCalls.remove(floor)) { served.add(new HallCall(floor, Direction.DOWN)); direction = Direction.DOWN; } else if (direction == Direction.UP && highestStop() <= floor && downCalls.remove(floor)) { // The top of this sweep: nothing above, so the down call here is next anyway. served.add(new HallCall(floor, Direction.DOWN)); direction = Direction.DOWN; } else if (direction == Direction.DOWN && lowestStop() >= floor && upCalls.remove(floor)) { served.add(new HallCall(floor, Direction.UP)); direction = Direction.UP; } return served; }
// LOOK: keep going while there is work ahead, then turn around. Every stop in the car's // way is served on this sweep, and everything else on the next, so no call waits forever. private Direction nextDirection() { boolean above = highestStop() > floor, below = lowestStop() < floor; if (direction == Direction.UP && above) return Direction.UP; if (direction == Direction.DOWN && below) return Direction.DOWN; if (above) return Direction.UP; if (below) return Direction.DOWN; return Direction.IDLE; }
// The farthest stops in each direction. With no stops, both are the current floor. int highestStop() { return max(max(max(floor, carStops), upCalls), downCalls); } int lowestStop() { return min(min(min(floor, carStops), upCalls), downCalls); }
private static int max(int f, TreeSet<Integer> s) { return s.isEmpty() ? f : Math.max(f, s.last()); } private static int min(int f, TreeSet<Integer> s) { return s.isEmpty() ? f : Math.min(f, s.first()); }
private void check(int f) { if (f < 0 || f >= floors) throw new IllegalArgumentException("no floor " + f); }
int floor() { return floor; } Direction direction() { return direction; } boolean doorsOpen() { return doorsOpen; }}
// Which way a car is heading. IDLE means it has nothing left to do.enum Direction { UP, DOWN, IDLE }
// A button pressed on a floor. It is a promise to arrive *going that way*, which is// why it carries a direction and a button inside the car does not.record HallCall(int floor, Direction direction) {}
// Picks which car answers a hall call. The rule changes from building to building// (and from morning to evening), so it is a strategy the system is given.interface Dispatcher { Car choose(List<Car> cars, HallCall call);}
// Sends the car that would arrive soonest, counted in floors travelled. A car already// heading toward the caller in the caller's direction is cheap; one that must finish its// sweep and come back pays for the whole detour.class NearestCarDispatcher implements Dispatcher { public Car choose(List<Car> cars, HallCall call) { // Ties go to the lower id, so the same situation always gets the same answer. return cars.stream() .min(Comparator.comparingInt((Car c) -> cost(c, call)).thenComparingInt(c -> c.id)) .orElseThrow(); }
static int cost(Car c, HallCall call) { int at = c.floor(), f = call.floor(); return switch (c.direction()) { case IDLE -> Math.abs(at - f); case UP -> { if (call.direction() == Direction.UP && f >= at) yield f - at; // on the way int turn = Math.max(c.highestStop(), f); // up to the top, then back yield (turn - at) + (turn - f); } case DOWN -> { if (call.direction() == Direction.DOWN && f <= at) yield at - f; int turn = Math.min(c.lowestStop(), f); yield (at - turn) + (f - turn); } }; }}
// The building's entry point. Buttons call it; it owns the cars and asks the dispatcher which car// answers each hall call. It also remembers every call it accepted and which car holds it: that// map makes a second press harmless and lets a broken car's calls move.class ElevatorSystem { private final List<Car> cars = new ArrayList<>(); private final Dispatcher dispatcher; private final Map<HallCall, Car> assigned = new HashMap<>(); // lit buttons -> who answers them private final Deque<HallCall> waiting = new ArrayDeque<>(); // accepted, but no car can take them yet
ElevatorSystem(int floors, int carCount, Dispatcher dispatcher) { for (int i = 0; i < carCount; i++) cars.add(new Car(i, floors)); this.dispatcher = dispatcher; }
void hallCall(int floor, Direction d) { HallCall call = new HallCall(floor, d); // People press a lit button again and again. Only the first press is a new call. if (assigned.containsKey(call) || waiting.contains(call)) return; if (!assign(call)) waiting.addLast(call); }
void carCall(int carId, int floor) { cars.get(carId).carCall(floor); }
// The car's calls go to other cars, or wait if none can take them. void takeOutOfService(int carId) { for (HallCall orphan : cars.get(carId).takeOutOfService()) { assigned.remove(orphan); if (!assign(orphan)) waiting.addLast(orphan); } }
void returnToService(int carId) { cars.get(carId).returnToService(); }
void setFull(int carId, boolean full) { cars.get(carId).setFull(full); }
void step() { // Calls that found no car get another chance every tick, oldest first. for (int i = waiting.size(); i > 0; i--) { HallCall c = waiting.removeFirst(); if (!assign(c)) waiting.addLast(c); } for (Car car : cars) for (HallCall served : car.step()) assigned.remove(served); }
// Dispatchers only ever see cars that can take the call, so no dispatcher has to // remember to skip broken or full cars. private boolean assign(HallCall call) { List<Car> candidates = cars.stream().filter(Car::accepting).toList(); if (candidates.isEmpty()) return false; Car car = dispatcher.choose(candidates, call); car.hallCall(call); assigned.put(call, car); return true; }
boolean isLit(HallCall call) { return assigned.containsKey(call) || waiting.contains(call); }
Car car(int id) { return cars.get(id); }}
// Everything that can happen to the building from outside, as values. Buttons, load sensors// and the maintenance panel run on their own threads; all they ever do is create one of these.sealed interface Event permits HallPressed, CarPressed, LoadChanged, ServiceChanged {}
record HallPressed(int floor, Direction direction) implements Event {}
record CarPressed(int carId, int floor) implements Event {}
record LoadChanged(int carId, boolean full) implements Event {}
record ServiceChanged(int carId, boolean inService) implements Event {}
// What a lobby display shows. Immutable, so any thread can read it without a lock.record CarStatus(int id, int floor, Direction direction, boolean doorsOpen) {}
// The one thread that owns the building. Other threads post events; only tick() touches// ElevatorSystem, so the cars, the dispatcher and the call map never need a lock.class Controller { private final ElevatorSystem system; private final int carCount; private final ConcurrentLinkedQueue<Event> inbox = new ConcurrentLinkedQueue<>(); // Replaced whole after every tick. volatile makes the new list visible to other threads. private volatile List<CarStatus> snapshot = List.of();
Controller(ElevatorSystem system, int carCount) { this.system = system; this.carCount = carCount; }
// Safe from any thread, and never blocks a button waiting for the loop. void post(Event e) { inbox.add(e); }
// Called by the control loop every tick, or directly by tests. Events are applied between // ticks, never in the middle of one, so the dispatcher always sees every car at rest. void tick() { for (Event e; (e = inbox.poll()) != null; ) { try { apply(e); } catch (IllegalArgumentException bad) { // A press for a floor that doesn't exist is dropped. Letting it escape would end // scheduleAtFixedRate's loop for good: one bad button would stop every car. } } system.step(); List<CarStatus> s = new ArrayList<>(); for (int i = 0; i < carCount; i++) { Car c = system.car(i); s.add(new CarStatus(i, c.floor(), c.direction(), c.doorsOpen())); } snapshot = List.copyOf(s); }
// Turns one event into one call on the building. private void apply(Event e) { switch (e) { case HallPressed h -> system.hallCall(h.floor(), h.direction()); case CarPressed c -> system.carCall(c.carId(), c.floor()); case LoadChanged l -> system.setFull(l.carId(), l.full()); case ServiceChanged s -> { if (s.inService()) system.returnToService(s.carId()); else system.takeOutOfService(s.carId()); } } }
List<CarStatus> snapshot() { return snapshot; }
// One thread at a fixed rate. The executor never runs two ticks at once, which is the // whole concurrency design: there is exactly one writer. ScheduledFuture<?> start(ScheduledExecutorService exec, long periodMs) { return exec.scheduleAtFixedRate(this::tick, 0, periodMs, TimeUnit.MILLISECONDS); }}# The complete design, built from every stage on this page.
from __future__ import annotations
import queueimport threadingfrom collections import dequefrom dataclasses import dataclassfrom enum import Enumfrom typing import Protocol
# One elevator car. It keeps its own stops and moves one tick at a time, sweeping up and down# (the LOOK rule below). A full car or one out of service gets no new hall calls.class Car: def __init__(self, id: int, floors: int): self.id = id self._floors = floors # floors are 0 .. floors - 1 self.floor = 0 self.direction = Direction.IDLE self.doors_open = False # Three kinds of stop. A car stop is served whichever way the car is going; # an up call only by a car going up. Plain sets: max() and min() over a handful of # floors is cheap, and a sorted container would add a dependency for nothing. self._car_stops: set[int] = set() self._up_calls: set[int] = set() self._down_calls: set[int] = set() self._in_service = True self.full = False # from the load sensor
# A full car still takes its riders where they asked, but shouldn't be sent for more. def accepting(self) -> bool: return self._in_service and not self.full
# Stop here with the doors open, and hand back the hall calls this car was holding so # the building can give them to another car. Without this, those people wait forever. def take_out_of_service(self) -> list[HallCall]: self._in_service = False self.doors_open = True self.direction = Direction.IDLE orphans = [HallCall(f, Direction.UP) for f in sorted(self._up_calls)] orphans += [HallCall(f, Direction.DOWN) for f in sorted(self._down_calls)] self._up_calls.clear() self._down_calls.clear() self._car_stops.clear() # riders step out here; their buttons no longer mean anything return orphans
def return_to_service(self) -> None: self._in_service = True # the doors close on the next tick
# A button inside the car: stop at f whichever way the car is going. def car_call(self, f: int) -> None: self._check(f) self._car_stops.add(f)
# A hall call the building gave this car: stop at that floor going that way. def hall_call(self, c: HallCall) -> None: self._check(c.floor) (self._up_calls if c.direction is Direction.UP else self._down_calls).add(c.floor)
# One tick: close the doors, or open them for the stops here, or move one floor. # Returns the hall calls answered this tick, so the building can turn their lights off. def step(self) -> list[HallCall]: if not self._in_service: return [] if self.doors_open: self.doors_open = False return [] car_stop = self.floor in self._car_stops # before _serve_here, which checks what is left above self._car_stops.discard(self.floor) served = self._serve_here() if car_stop or served: self.doors_open = True return served self.direction = self._next_direction() if self.direction is Direction.UP: self.floor += 1 elif self.direction is Direction.DOWN: self.floor -= 1 return []
# The hall calls this floor can answer, given the way the car is heading. def _serve_here(self) -> list[HallCall]: f, d = self.floor, self.direction if d is not Direction.DOWN and f in self._up_calls: return self._take(self._up_calls, Direction.UP) if d is not Direction.UP and f in self._down_calls: return self._take(self._down_calls, Direction.DOWN) # The top of this sweep: nothing above, so the down call here is next anyway. if d is Direction.UP and self.highest_stop() <= f and f in self._down_calls: return self._take(self._down_calls, Direction.DOWN) if d is Direction.DOWN and self.lowest_stop() >= f and f in self._up_calls: return self._take(self._up_calls, Direction.UP) return []
def _take(self, calls: set[int], d: Direction) -> list[HallCall]: calls.discard(self.floor) self.direction = d return [HallCall(self.floor, d)]
# LOOK: keep going while there is work ahead, then turn around. Every stop in the car's # way is served on this sweep, and everything else on the next, so no call waits forever. def _next_direction(self) -> Direction: above, below = self.highest_stop() > self.floor, self.lowest_stop() < self.floor if self.direction is Direction.UP and above: return Direction.UP if self.direction is Direction.DOWN and below: return Direction.DOWN if above: return Direction.UP if below: return Direction.DOWN return Direction.IDLE
# The farthest stops in each direction. With no stops, both are the current floor. def highest_stop(self) -> int: return max([self.floor, *self._car_stops, *self._up_calls, *self._down_calls])
def lowest_stop(self) -> int: return min([self.floor, *self._car_stops, *self._up_calls, *self._down_calls])
def _check(self, f: int) -> None: if not 0 <= f < self._floors: raise ValueError(f"no floor {f}")
# Which way a car is heading. IDLE means it has nothing left to do.class Direction(Enum): UP = "up" DOWN = "down" IDLE = "idle"
# A button pressed on a floor. It is a promise to arrive *going that way*, which is# why it carries a direction and a button inside the car does not.@dataclass(frozen=True)class HallCall: floor: int direction: Direction
# Picks which car answers a hall call. The rule changes from building to building# (and from morning to evening), so it is a strategy the system is given.class Dispatcher(Protocol): def choose(self, cars: list[Car], call: HallCall) -> Car: ...
# Sends the car that would arrive soonest, counted in floors travelled. A car already# heading toward the caller in the caller's direction is cheap; one that must finish its# sweep and come back pays for the whole detour.class NearestCarDispatcher: def choose(self, cars: list[Car], call: HallCall) -> Car: # Ties go to the lower id, so the same situation always gets the same answer. return min(cars, key=lambda c: (self.cost(c, call), c.id))
@staticmethod def cost(c: Car, call: HallCall) -> int: at, f = c.floor, call.floor if c.direction is Direction.IDLE: return abs(at - f) if c.direction is Direction.UP: if call.direction is Direction.UP and f >= at: return f - at # on the way turn = max(c.highest_stop(), f) # up to the top, then back return (turn - at) + (turn - f) if call.direction is Direction.DOWN and f <= at: return at - f turn = min(c.lowest_stop(), f) return (at - turn) + (f - turn)
# The building's entry point. Buttons call it; it owns the cars and asks the dispatcher which# car answers each hall call. It also remembers every call it accepted and which car holds it:# that map makes a second press harmless and lets a broken car's calls move.class ElevatorSystem: def __init__(self, floors: int, car_count: int, dispatcher: Dispatcher): self._cars = [Car(i, floors) for i in range(car_count)] self._dispatcher = dispatcher self._assigned: dict[HallCall, Car] = {} # lit buttons -> who answers them self._waiting: deque[HallCall] = deque() # accepted, but no car can take them yet
def hall_call(self, floor: int, d: Direction) -> None: call = HallCall(floor, d) # People press a lit button again and again. Only the first press is a new call. if self.is_lit(call): return if not self._assign(call): self._waiting.append(call)
def car_call(self, car_id: int, floor: int) -> None: self._cars[car_id].car_call(floor)
# The car's calls go to other cars, or wait if none can take them. def take_out_of_service(self, car_id: int) -> None: for orphan in self._cars[car_id].take_out_of_service(): self._assigned.pop(orphan, None) if not self._assign(orphan): self._waiting.append(orphan)
def return_to_service(self, car_id: int) -> None: self._cars[car_id].return_to_service()
def set_full(self, car_id: int, full: bool) -> None: self._cars[car_id].full = full
def step(self) -> None: # Calls that found no car get another chance every tick, oldest first. for _ in range(len(self._waiting)): c = self._waiting.popleft() if not self._assign(c): self._waiting.append(c) for car in self._cars: for served in car.step(): self._assigned.pop(served, None)
# Dispatchers only ever see cars that can take the call, so no dispatcher has to # remember to skip broken or full cars. def _assign(self, call: HallCall) -> bool: candidates = [c for c in self._cars if c.accepting()] if not candidates: return False car = self._dispatcher.choose(candidates, call) car.hall_call(call) self._assigned[call] = car return True
def is_lit(self, call: HallCall) -> bool: return call in self._assigned or call in self._waiting
def car(self, id: int) -> Car: return self._cars[id]
# Everything that can happen to the building from outside, as values. Buttons, load sensors# and the maintenance panel run on their own threads; all they ever do is create one of these.@dataclass(frozen=True)class HallPressed: floor: int direction: Direction
@dataclass(frozen=True)class CarPressed: car_id: int floor: int
@dataclass(frozen=True)class LoadChanged: car_id: int full: bool
@dataclass(frozen=True)class ServiceChanged: car_id: int in_service: bool
Event = HallPressed | CarPressed | LoadChanged | ServiceChanged
# What a lobby display shows. Immutable, so any thread can read it without a lock.@dataclass(frozen=True)class CarStatus: id: int floor: int direction: Direction doors_open: bool
# The one thread that owns the building. Other threads post events; only tick() touches# ElevatorSystem, so the cars, the dispatcher and the call map never need a lock.class Controller: def __init__(self, system: ElevatorSystem, car_count: int): self._system = system self._car_count = car_count self._inbox: queue.SimpleQueue[Event] = queue.SimpleQueue() # thread-safe, never blocks a put # Replaced whole after every tick. Rebinding one attribute is atomic in CPython, # so a reader gets the old tuple or the new one, never half of each. self.snapshot: tuple[CarStatus, ...] = ()
# Safe from any thread, and never blocks a button waiting for the loop. def post(self, e: Event) -> None: self._inbox.put(e)
# Called by the control loop every tick, or directly by tests. Events are applied between # ticks, never in the middle of one, so the dispatcher always sees every car at rest. def tick(self) -> None: while True: try: e = self._inbox.get_nowait() except queue.Empty: break try: self._apply(e) except ValueError: pass # a press for a floor that doesn't exist is dropped, not allowed to stop the loop self._system.step() self.snapshot = tuple( CarStatus(i, c.floor, c.direction, c.doors_open) for i in range(self._car_count) for c in [self._system.car(i)] )
# Turns one event into one call on the building. def _apply(self, e: Event) -> None: match e: case HallPressed(floor, direction): self._system.hall_call(floor, direction) case CarPressed(car_id, floor): self._system.car_call(car_id, floor) case LoadChanged(car_id, full): self._system.set_full(car_id, full) case ServiceChanged(car_id, True): self._system.return_to_service(car_id) case ServiceChanged(car_id, False): self._system.take_out_of_service(car_id)
# One thread at a fixed rate: there is exactly one writer, which is the whole design. def run(self, period_s: float, stop: threading.Event) -> None: while not stop.wait(period_s): self.tick()// The complete design, built from every stage on this page.
using System.Collections.Concurrent;
// One elevator car. It keeps its own stops and moves one tick at a time, sweeping up and down// (the LOOK rule below). A full car or one out of service gets no new hall calls.public sealed class Car(int id, int floors) // floors are 0 .. floors - 1{ public int Id { get; } = id; public int Floor { get; private set; } public Direction Direction { get; private set; } = Direction.Idle; public bool DoorsOpen { get; private set; }
// Three kinds of stop, each a sorted set so "anything above me?" is one lookup. // A car stop is served whichever way the car is going. An up call only by a car going up. private readonly SortedSet<int> _carStops = []; private readonly SortedSet<int> _upCalls = []; private readonly SortedSet<int> _downCalls = []; private bool _inService = true;
public bool Full { get; set; } // from the load sensor
// A full car still takes its riders where they asked, but shouldn't be sent for more. public bool Accepting => _inService && !Full;
// Stop here with the doors open, and hand back the hall calls this car was holding so // the building can give them to another car. Without this, those people wait forever. public List<HallCall> TakeOutOfService() { _inService = false; DoorsOpen = true; Direction = Direction.Idle; var orphans = _upCalls.Select(f => new HallCall(f, Direction.Up)) .Concat(_downCalls.Select(f => new HallCall(f, Direction.Down))) .ToList(); _upCalls.Clear(); _downCalls.Clear(); _carStops.Clear(); // riders step out here; their buttons no longer mean anything return orphans; }
public void ReturnToService() => _inService = true; // the doors close on the next tick
// A button inside the car: stop at f whichever way the car is going. public void CarCall(int f) { Check(f); _carStops.Add(f); }
// A hall call the building gave this car: stop at that floor going that way. public void HallCall(HallCall c) { Check(c.Floor); (c.Direction == Direction.Up ? _upCalls : _downCalls).Add(c.Floor); }
// One tick: close the doors, or open them for the stops here, or move one floor. // Returns the hall calls answered this tick, so the building can turn their lights off. public IReadOnlyList<HallCall> Step() { if (!_inService) return []; if (DoorsOpen) { DoorsOpen = false; return []; } bool carStop = _carStops.Remove(Floor); // before ServeHere, which checks what is left above var served = ServeHere(); if (carStop || served.Count > 0) { DoorsOpen = true; return served; } Direction = NextDirection(); if (Direction == Direction.Up) Floor++; else if (Direction == Direction.Down) Floor--; return []; }
// The hall calls this floor can answer, given the way the car is heading. private List<HallCall> ServeHere() { if (Direction != Direction.Down && _upCalls.Contains(Floor)) return Take(_upCalls, Direction.Up); if (Direction != Direction.Up && _downCalls.Contains(Floor)) return Take(_downCalls, Direction.Down); // The top of this sweep: nothing above, so the down call here is next anyway. if (Direction == Direction.Up && HighestStop <= Floor && _downCalls.Contains(Floor)) return Take(_downCalls, Direction.Down); if (Direction == Direction.Down && LowestStop >= Floor && _upCalls.Contains(Floor)) return Take(_upCalls, Direction.Up); return []; }
private List<HallCall> Take(SortedSet<int> calls, Direction d) { calls.Remove(Floor); Direction = d; return [new HallCall(Floor, d)]; }
// LOOK: keep going while there is work ahead, then turn around. Every stop in the car's // way is served on this sweep, and everything else on the next, so no call waits forever. private Direction NextDirection() { bool above = HighestStop > Floor, below = LowestStop < Floor; if (Direction == Direction.Up && above) return Direction.Up; if (Direction == Direction.Down && below) return Direction.Down; if (above) return Direction.Up; if (below) return Direction.Down; return Direction.Idle; }
// The farthest stops in each direction. With no stops, both are the current floor. public int HighestStop => Math.Max(Floor, Math.Max(Max(_carStops), Math.Max(Max(_upCalls), Max(_downCalls)))); public int LowestStop => Math.Min(Floor, Math.Min(Min(_carStops), Math.Min(Min(_upCalls), Min(_downCalls))));
private static int Max(SortedSet<int> s) => s.Count == 0 ? int.MinValue : s.Max; private static int Min(SortedSet<int> s) => s.Count == 0 ? int.MaxValue : s.Min;
private void Check(int f) { if (f < 0 || f >= floors) throw new ArgumentOutOfRangeException(nameof(f), $"no floor {f}"); }}
// Which way a car is heading. Idle means it has nothing left to do.public enum Direction { Up, Down, Idle }
// A button pressed on a floor. It is a promise to arrive *going that way*, which is// why it carries a direction and a button inside the car does not.public readonly record struct HallCall(int Floor, Direction Direction);
// Picks which car answers a hall call. The rule changes from building to building// (and from morning to evening), so it is a strategy the system is given.public interface IDispatcher{ Car Choose(IReadOnlyList<Car> cars, HallCall call);}
// Sends the car that would arrive soonest, counted in floors travelled. A car already// heading toward the caller in the caller's direction is cheap; one that must finish its// sweep and come back pays for the whole detour.public sealed class NearestCarDispatcher : IDispatcher{ // Ties go to the lower id, so the same situation always gets the same answer. public Car Choose(IReadOnlyList<Car> cars, HallCall call) => cars.OrderBy(c => Cost(c, call)).ThenBy(c => c.Id).First();
public static int Cost(Car c, HallCall call) { int at = c.Floor, f = call.Floor; switch (c.Direction) { case Direction.Up: { if (call.Direction == Direction.Up && f >= at) return f - at; // on the way int turn = Math.Max(c.HighestStop, f); // up to the top, then back return (turn - at) + (turn - f); } case Direction.Down: { if (call.Direction == Direction.Down && f <= at) return at - f; int turn = Math.Min(c.LowestStop, f); return (at - turn) + (f - turn); } default: return Math.Abs(at - f); } }}
// The building's entry point. Buttons call it; it owns the cars and asks the dispatcher which// car answers each hall call. It also remembers every call it accepted and which car holds it:// that map makes a second press harmless and lets a broken car's calls move.public sealed class ElevatorSystem{ private readonly List<Car> _cars; private readonly IDispatcher _dispatcher; private readonly Dictionary<HallCall, Car> _assigned = new(); // lit buttons -> who answers them private readonly LinkedList<HallCall> _waiting = new(); // accepted, but no car can take them yet
public ElevatorSystem(int floors, int carCount, IDispatcher dispatcher) { _cars = Enumerable.Range(0, carCount).Select(i => new Car(i, floors)).ToList(); _dispatcher = dispatcher; }
public void HallCall(int floor, Direction d) { var call = new HallCall(floor, d); // People press a lit button again and again. Only the first press is a new call. if (IsLit(call)) return; if (!Assign(call)) _waiting.AddLast(call); }
public void CarCall(int carId, int floor) => _cars[carId].CarCall(floor);
// The car's calls go to other cars, or wait if none can take them. public void TakeOutOfService(int carId) { foreach (var orphan in _cars[carId].TakeOutOfService()) { _assigned.Remove(orphan); if (!Assign(orphan)) _waiting.AddLast(orphan); } }
public void ReturnToService(int carId) => _cars[carId].ReturnToService();
public void SetFull(int carId, bool full) => _cars[carId].Full = full;
public void Step() { // Calls that found no car get another chance every tick, oldest first. for (int i = _waiting.Count; i > 0; i--) { var c = _waiting.First!.Value; _waiting.RemoveFirst(); if (!Assign(c)) _waiting.AddLast(c); } foreach (var car in _cars) foreach (var served in car.Step()) _assigned.Remove(served); }
// Dispatchers only ever see cars that can take the call, so no dispatcher has to // remember to skip broken or full cars. private bool Assign(HallCall call) { var candidates = _cars.Where(c => c.Accepting).ToList(); if (candidates.Count == 0) return false; var car = _dispatcher.Choose(candidates, call); car.HallCall(call); _assigned[call] = car; return true; }
public bool IsLit(HallCall call) => _assigned.ContainsKey(call) || _waiting.Contains(call);
public Car Car(int id) => _cars[id];}
// Everything that can happen to the building from outside, as values. Buttons, load sensors// and the maintenance panel run on their own threads; all they ever do is create one of these.public abstract record BuildingEvent;
public sealed record HallPressed(int Floor, Direction Direction) : BuildingEvent;
public sealed record CarPressed(int CarId, int Floor) : BuildingEvent;
public sealed record LoadChanged(int CarId, bool Full) : BuildingEvent;
public sealed record ServiceChanged(int CarId, bool InService) : BuildingEvent;
// What a lobby display shows. Immutable, so any thread can read it without a lock.public sealed record CarStatus(int Id, int Floor, Direction Direction, bool DoorsOpen);
// The one thread that owns the building. Other threads post events; only Tick() touches// ElevatorSystem, so the cars, the dispatcher and the call map never need a lock.public sealed class Controller(ElevatorSystem system, int carCount){ private readonly ConcurrentQueue<BuildingEvent> _inbox = new(); // Replaced whole after every tick. volatile makes the new list visible to other threads. private volatile IReadOnlyList<CarStatus> _snapshot = [];
// Safe from any thread, and never blocks a button waiting for the loop. public void Post(BuildingEvent e) => _inbox.Enqueue(e);
// Called by the control loop every tick, or directly by tests. Events are applied between // ticks, never in the middle of one, so the dispatcher always sees every car at rest. public void Tick() { while (_inbox.TryDequeue(out var e)) { try { Apply(e); } catch (ArgumentOutOfRangeException) { // A press for a floor that doesn't exist is dropped. Letting it escape would end // the loop below: one bad button would stop every car. } } system.Step(); _snapshot = Enumerable.Range(0, carCount) .Select(i => system.Car(i)) .Select(c => new CarStatus(c.Id, c.Floor, c.Direction, c.DoorsOpen)) .ToList(); }
// Turns one event into one call on the building. private void Apply(BuildingEvent e) { switch (e) { case HallPressed h: system.HallCall(h.Floor, h.Direction); break; case CarPressed c: system.CarCall(c.CarId, c.Floor); break; case LoadChanged l: system.SetFull(l.CarId, l.Full); break; case ServiceChanged { InService: true } s: system.ReturnToService(s.CarId); break; case ServiceChanged s: system.TakeOutOfService(s.CarId); break; } }
public IReadOnlyList<CarStatus> Snapshot => _snapshot;
// One loop, one tick at a time: PeriodicTimer never overlaps ticks, which is the whole // concurrency design. There is exactly one writer. public async Task RunAsync(TimeSpan period, CancellationToken stop) { using var timer = new PeriodicTimer(period); while (await timer.WaitForNextTickAsync(stop)) Tick(); }}// The complete design, built from every stage on this page.
package elevator
import ( "cmp" "fmt" "maps" "slices" "sync/atomic" "time")
// Direction is which way a car is heading. Idle means it has nothing left to do.type Direction int
const ( Idle Direction = iota Up Down)
// HallCall is a button pressed on a floor. It is a promise to arrive going that way,// which is why it carries a direction and a button inside the car does not.type HallCall struct { Floor int Direction Direction}
// Dispatcher picks which car answers a hall call. The rule changes from building to building// (and from morning to evening), so it is a strategy the system is given.type Dispatcher interface { Choose(cars []*Car, call HallCall) *Car}
// NearestCar sends the car that would arrive soonest, counted in floors travelled. A car// already heading toward the caller in the caller's direction is cheap; one that must finish// its sweep and come back pays for the whole detour.type NearestCar struct{}
func (NearestCar) Choose(cars []*Car, call HallCall) *Car { // Ties go to the lower id (the first in the slice), so the same situation always gets // the same answer. return slices.MinFunc(cars, func(a, b *Car) int { return cmp.Or(cmp.Compare(Cost(a, call), Cost(b, call)), cmp.Compare(a.ID, b.ID)) })}
func Cost(c *Car, call HallCall) int { at, f := c.Floor, call.Floor switch c.Direction { case Up: if call.Direction == Up && f >= at { return f - at // on the way } turn := max(c.HighestStop(), f) // up to the top, then back return (turn - at) + (turn - f) case Down: if call.Direction == Down && f <= at { return at - f } turn := min(c.LowestStop(), f) return (at - turn) + (f - turn) } if at > f { return at - f } return f - at}
// One elevator car. It keeps its own stops and moves one tick at a time, sweeping up and down// (the LOOK rule below). A full car or one out of service gets no new hall calls.type Car struct { ID int floors int // floors are 0 .. floors-1 Floor int Direction Direction DoorsOpen bool // Three kinds of stop. A car stop is served whichever way the car is going; // an up call only by a car going up. Maps as sets: a building has tens of floors, // so scanning them for the highest stop costs nothing. carStops, upCalls, downCalls map[int]bool inService bool Full bool // from the load sensor}
func NewCar(id, floors int) *Car { return &Car{ID: id, floors: floors, inService: true, carStops: map[int]bool{}, upCalls: map[int]bool{}, downCalls: map[int]bool{}}}
// Accepting: a full car still takes its riders where they asked, but shouldn't be sent for more.func (c *Car) Accepting() bool { return c.inService && !c.Full }
// TakeOutOfService stops the car here with the doors open, and hands back the hall calls it// was holding so the building can give them to another car. Without this, those people// wait forever.func (c *Car) TakeOutOfService() []HallCall { c.inService, c.DoorsOpen, c.Direction = false, true, Idle var orphans []HallCall for _, f := range slices.Sorted(maps.Keys(c.upCalls)) { orphans = append(orphans, HallCall{f, Up}) } for _, f := range slices.Sorted(maps.Keys(c.downCalls)) { orphans = append(orphans, HallCall{f, Down}) } clear(c.upCalls) clear(c.downCalls) clear(c.carStops) // riders step out here; their buttons no longer mean anything return orphans}
func (c *Car) ReturnToService() { c.inService = true } // the doors close on the next tick
// CarCall is a button inside the car: stop at f whichever way the car is going.func (c *Car) CarCall(f int) error { if err := c.check(f); err != nil { return err } c.carStops[f] = true return nil}
// HallCall is a call the building gave this car: stop at that floor going that way.func (c *Car) HallCall(h HallCall) error { if err := c.check(h.Floor); err != nil { return err } if h.Direction == Up { c.upCalls[h.Floor] = true } else { c.downCalls[h.Floor] = true } return nil}
// Step does one thing: close the doors, or open them for the stops here, or move one floor.// It returns the hall calls answered this tick, so the building can turn their lights off.func (c *Car) Step() []HallCall { if !c.inService { return nil } if c.DoorsOpen { c.DoorsOpen = false return nil } carStop := c.carStops[c.Floor] // before serveHere, which checks what is left above delete(c.carStops, c.Floor) served := c.serveHere() if carStop || len(served) > 0 { c.DoorsOpen = true return served } c.Direction = c.nextDirection() switch c.Direction { case Up: c.Floor++ case Down: c.Floor-- } return nil}
// serveHere answers the hall calls this floor can take, given the way the car is heading.func (c *Car) serveHere() []HallCall { f, d := c.Floor, c.Direction switch { case d != Down && c.upCalls[f]: return c.take(c.upCalls, Up) case d != Up && c.downCalls[f]: return c.take(c.downCalls, Down) case d == Up && c.HighestStop() <= f && c.downCalls[f]: // The top of this sweep: nothing above, so the down call here is next anyway. return c.take(c.downCalls, Down) case d == Down && c.LowestStop() >= f && c.upCalls[f]: return c.take(c.upCalls, Up) } return nil}
func (c *Car) take(calls map[int]bool, d Direction) []HallCall { delete(calls, c.Floor) c.Direction = d return []HallCall{{c.Floor, d}}}
// nextDirection is LOOK: keep going while there is work ahead, then turn around. Every stop// in the car's way is served on this sweep, and everything else on the next, so no call// waits forever.func (c *Car) nextDirection() Direction { above, below := c.HighestStop() > c.Floor, c.LowestStop() < c.Floor switch { case c.Direction == Up && above: return Up case c.Direction == Down && below: return Down case above: return Up case below: return Down } return Idle}
// HighestStop and LowestStop are the farthest stops each way; the current floor if none.func (c *Car) HighestStop() int { h := c.Floor for _, set := range []map[int]bool{c.carStops, c.upCalls, c.downCalls} { for f := range set { h = max(h, f) } } return h}
func (c *Car) LowestStop() int { l := c.Floor for _, set := range []map[int]bool{c.carStops, c.upCalls, c.downCalls} { for f := range set { l = min(l, f) } } return l}
func (c *Car) check(f int) error { if f < 0 || f >= c.floors { return fmt.Errorf("no floor %d", f) } return nil}
// ElevatorSystem is the building's entry point. Buttons call it; it owns the cars and asks the dispatcher which// car answers each hall call. It also remembers every call it accepted and which car holds it:// that map makes a second press harmless and lets a broken car's calls move.type ElevatorSystem struct { cars []*Car dispatcher Dispatcher assigned map[HallCall]*Car // lit buttons -> who answers them waiting []HallCall // accepted, but no car can take them yet}
func NewElevatorSystem(floors, carCount int, d Dispatcher) *ElevatorSystem { s := &ElevatorSystem{dispatcher: d, assigned: map[HallCall]*Car{}} for i := range carCount { s.cars = append(s.cars, NewCar(i, floors)) } return s}
func (s *ElevatorSystem) HallCall(floor int, d Direction) error { call := HallCall{floor, d} // People press a lit button again and again. Only the first press is a new call. if s.IsLit(call) { return nil } ok, err := s.assign(call) if err == nil && !ok { s.waiting = append(s.waiting, call) } return err}
func (s *ElevatorSystem) CarCall(carID, floor int) error { return s.cars[carID].CarCall(floor) }
// TakeOutOfService gives the car's calls to other cars, or makes them wait if none can take them.func (s *ElevatorSystem) TakeOutOfService(carID int) { for _, orphan := range s.cars[carID].TakeOutOfService() { delete(s.assigned, orphan) if ok, _ := s.assign(orphan); !ok { s.waiting = append(s.waiting, orphan) } }}
func (s *ElevatorSystem) ReturnToService(carID int) { s.cars[carID].ReturnToService() }
func (s *ElevatorSystem) SetFull(carID int, full bool) { s.cars[carID].Full = full }
func (s *ElevatorSystem) Step() { // Calls that found no car get another chance every tick, oldest first. still := s.waiting[:0] for _, c := range s.waiting { if ok, _ := s.assign(c); !ok { still = append(still, c) } } s.waiting = still for _, car := range s.cars { for _, served := range car.Step() { delete(s.assigned, served) } }}
// assign shows dispatchers only the cars that can take the call, so no dispatcher has to// remember to skip broken or full cars.func (s *ElevatorSystem) assign(call HallCall) (bool, error) { var candidates []*Car for _, c := range s.cars { if c.Accepting() { candidates = append(candidates, c) } } if len(candidates) == 0 { return false, nil } car := s.dispatcher.Choose(candidates, call) if err := car.HallCall(call); err != nil { return false, err } s.assigned[call] = car return true, nil}
func (s *ElevatorSystem) IsLit(call HallCall) bool { _, ok := s.assigned[call] return ok || slices.Contains(s.waiting, call)}
func (s *ElevatorSystem) Car(id int) *Car { return s.cars[id] }
// Event is everything that can happen to the building from outside, as values. Buttons, load// sensors and the maintenance panel run on their own goroutines; all they do is send one.type Event interface{ isEvent() }
type HallPressed struct { Floor int Direction Direction}
type CarPressed struct{ CarID, Floor int }
type LoadChanged struct { CarID int Full bool}
type ServiceChanged struct { CarID int InService bool}
func (HallPressed) isEvent() {}
func (CarPressed) isEvent() {}
func (LoadChanged) isEvent() {}
func (ServiceChanged) isEvent() {}
// CarStatus is what a lobby display shows. A copy, so any goroutine can keep it.type CarStatus struct { ID, Floor int Direction Direction DoorsOpen bool}
// Controller is the one goroutine that owns the building. Others send events on a channel;// only Tick touches the ElevatorSystem, so the cars, the dispatcher and the call map never// need a mutex. "Share memory by communicating."type Controller struct { system *ElevatorSystem carCount int Inbox chan Event // buffered, so a button rarely waits snapshot atomic.Pointer[[]CarStatus] // replaced whole after every tick}
func NewController(s *ElevatorSystem, carCount int) *Controller { c := &Controller{system: s, carCount: carCount, Inbox: make(chan Event, 256)} c.snapshot.Store(&[]CarStatus{}) return c}
// Tick applies every event that has arrived, then moves the cars one step. Events land// between ticks, never in the middle of one, so the dispatcher always sees every car at rest.func (c *Controller) Tick() { for drained := false; !drained; { select { case e := <-c.Inbox: // A press for a floor that doesn't exist comes back as an error and is dropped. _ = c.apply(e) default: drained = true } } c.system.Step() s := make([]CarStatus, c.carCount) for i := range s { car := c.system.Car(i) s[i] = CarStatus{i, car.Floor, car.Direction, car.DoorsOpen} } c.snapshot.Store(&s)}
// apply turns one event into one call on the building.func (c *Controller) apply(e Event) error { switch e := e.(type) { case HallPressed: return c.system.HallCall(e.Floor, e.Direction) case CarPressed: return c.system.CarCall(e.CarID, e.Floor) case LoadChanged: c.system.SetFull(e.CarID, e.Full) case ServiceChanged: if e.InService { c.system.ReturnToService(e.CarID) } else { c.system.TakeOutOfService(e.CarID) } } return nil}
func (c *Controller) Snapshot() []CarStatus { return *c.snapshot.Load() }
// Run ticks at a fixed rate until stop is closed. One goroutine, one writer: that is the// whole concurrency design.func (c *Controller) Run(period time.Duration, stop <-chan struct{}) { t := time.NewTicker(period) defer t.Stop() for { select { case <-t.C: c.Tick() case <-stop: return } }}// The complete design, built from every stage on this page.
// One elevator car. It keeps its own stops and moves one tick at a time, sweeping up and down// (the LOOK rule below). A full car or one out of service gets no new hall calls.class Car { floor = 0; direction: Direction = 'idle'; doorsOpen = false; // Three kinds of stop. A car stop is served whichever way the car is going; // an up call only by a car going up. private readonly carStops = new Set<number>(); private readonly upCalls = new Set<number>(); private readonly downCalls = new Set<number>(); private inService = true; full = false; // from the load sensor
constructor( readonly id: number, private readonly floors: number, // floors are 0 .. floors - 1 ) {}
// A full car still takes its riders where they asked, but shouldn't be sent for more. accepting(): boolean { return this.inService && !this.full; }
// Stop here with the doors open, and hand back the hall calls this car was holding so // the building can give them to another car. Without this, those people wait forever. takeOutOfService(): HallCall[] { this.inService = false; this.doorsOpen = true; this.direction = 'idle'; const sorted = (s: Set<number>) => [...s].sort((a, b) => a - b); const orphans: HallCall[] = [ ...sorted(this.upCalls).map((floor) => ({ floor, direction: 'up' as const })), ...sorted(this.downCalls).map((floor) => ({ floor, direction: 'down' as const })), ]; this.upCalls.clear(); this.downCalls.clear(); this.carStops.clear(); // riders step out here; their buttons no longer mean anything return orphans; }
returnToService(): void { this.inService = true; // the doors close on the next tick }
// A button inside the car: stop at f whichever way the car is going. carCall(f: number): void { this.check(f); this.carStops.add(f); }
// A hall call the building gave this car: stop at that floor going that way. hallCall(c: HallCall): void { this.check(c.floor); (c.direction === 'up' ? this.upCalls : this.downCalls).add(c.floor); }
// One tick: close the doors, or open them for the stops here, or move one floor. // Returns the hall calls answered this tick, so the building can turn their lights off. step(): HallCall[] { if (!this.inService) return []; if (this.doorsOpen) { this.doorsOpen = false; return []; } const carStop = this.carStops.delete(this.floor); // before serveHere, which checks what is left above const served = this.serveHere(); if (carStop || served.length > 0) { this.doorsOpen = true; return served; } this.direction = this.nextDirection(); if (this.direction === 'up') this.floor++; else if (this.direction === 'down') this.floor--; return []; }
// The hall calls this floor can answer, given the way the car is heading. private serveHere(): HallCall[] { const f = this.floor; const d = this.direction; if (d !== 'down' && this.upCalls.has(f)) return this.take(this.upCalls, 'up'); if (d !== 'up' && this.downCalls.has(f)) return this.take(this.downCalls, 'down'); // The top of this sweep: nothing above, so the down call here is next anyway. if (d === 'up' && this.highestStop() <= f && this.downCalls.has(f)) return this.take(this.downCalls, 'down'); if (d === 'down' && this.lowestStop() >= f && this.upCalls.has(f)) return this.take(this.upCalls, 'up'); return []; }
private take(calls: Set<number>, d: 'up' | 'down'): HallCall[] { calls.delete(this.floor); this.direction = d; return [{ floor: this.floor, direction: d }]; }
// LOOK: keep going while there is work ahead, then turn around. Every stop in the car's // way is served on this sweep, and everything else on the next, so no call waits forever. private nextDirection(): Direction { const above = this.highestStop() > this.floor; const below = this.lowestStop() < this.floor; if (this.direction === 'up' && above) return 'up'; if (this.direction === 'down' && below) return 'down'; if (above) return 'up'; if (below) return 'down'; return 'idle'; }
// The farthest stops in each direction. With no stops, both are the current floor. highestStop(): number { return Math.max(this.floor, ...this.carStops, ...this.upCalls, ...this.downCalls); }
lowestStop(): number { return Math.min(this.floor, ...this.carStops, ...this.upCalls, ...this.downCalls); }
private check(f: number): void { if (!Number.isInteger(f) || f < 0 || f >= this.floors) throw new RangeError(`no floor ${f}`); }}
// Which way a car is heading. 'idle' means it has nothing left to do.type Direction = 'up' | 'down' | 'idle';
// A button pressed on a floor. It is a promise to arrive *going that way*, which is// why it carries a direction and a button inside the car does not.interface HallCall { readonly floor: number; readonly direction: 'up' | 'down';}
// Picks which car answers a hall call. The rule changes from building to building// (and from morning to evening), so it is a strategy the system is given.interface Dispatcher { choose(cars: readonly Car[], call: HallCall): Car;}
// Sends the car that would arrive soonest, counted in floors travelled. A car already// heading toward the caller in the caller's direction is cheap; one that must finish its// sweep and come back pays for the whole detour.class NearestCarDispatcher implements Dispatcher { choose(cars: readonly Car[], call: HallCall): Car { // Ties go to the lower id, so the same situation always gets the same answer. return cars.reduce((best, c) => { const d = NearestCarDispatcher.cost(c, call) - NearestCarDispatcher.cost(best, call); return d < 0 || (d === 0 && c.id < best.id) ? c : best; }); }
static cost(c: Car, call: HallCall): number { const at = c.floor; const f = call.floor; if (c.direction === 'idle') return Math.abs(at - f); if (c.direction === 'up') { if (call.direction === 'up' && f >= at) return f - at; // on the way const turn = Math.max(c.highestStop(), f); // up to the top, then back return turn - at + (turn - f); } if (call.direction === 'down' && f <= at) return at - f; const turn = Math.min(c.lowestStop(), f); return at - turn + (f - turn); }}
// The building's entry point. Buttons call it; it owns the cars and asks the dispatcher which// car answers each hall call. It also remembers every call it accepted and which car holds it:// that map makes a second press harmless and lets a broken car's calls move.class ElevatorSystem { private readonly cars: Car[]; private readonly assigned = new Map<string, Car>(); // lit buttons -> who answers them private waiting: HallCall[] = []; // accepted, but no car can take them yet
constructor( floors: number, carCount: number, private readonly dispatcher: Dispatcher, ) { this.cars = Array.from({ length: carCount }, (_, i) => new Car(i, floors)); }
hallCall(floor: number, direction: 'up' | 'down'): void { const call = { floor, direction }; // People press a lit button again and again. Only the first press is a new call. if (this.isLit(call)) return; if (!this.assign(call)) this.waiting.push(call); }
carCall(carId: number, floor: number): void { this.cars[carId].carCall(floor); }
// The car's calls go to other cars, or wait if none can take them. takeOutOfService(carId: number): void { for (const orphan of this.cars[carId].takeOutOfService()) { this.assigned.delete(keyOf(orphan)); if (!this.assign(orphan)) this.waiting.push(orphan); } }
returnToService(carId: number): void { this.cars[carId].returnToService(); }
setFull(carId: number, full: boolean): void { this.cars[carId].full = full; }
step(): void { // Calls that found no car get another chance every tick, oldest first. this.waiting = this.waiting.filter((c) => !this.assign(c)); for (const car of this.cars) for (const served of car.step()) this.assigned.delete(keyOf(served)); }
// Dispatchers only ever see cars that can take the call, so no dispatcher has to // remember to skip broken or full cars. private assign(call: HallCall): boolean { const candidates = this.cars.filter((c) => c.accepting()); if (candidates.length === 0) return false; const car = this.dispatcher.choose(candidates, call); car.hallCall(call); this.assigned.set(keyOf(call), car); return true; }
isLit(call: HallCall): boolean { return this.assigned.has(keyOf(call)) || this.waiting.some((w) => keyOf(w) === keyOf(call)); }
car(id: number): Car { return this.cars[id]; }}
// A hall call as a map key. Objects compare by reference in JavaScript, so two presses of// the same button need a string that is equal whenever the calls are.const keyOf = (c: HallCall) => `${c.floor}:${c.direction}`;
// Everything that can happen to the building from outside, as values.type BuildingEvent = | { kind: 'hall'; floor: number; direction: 'up' | 'down' } | { kind: 'car'; carId: number; floor: number } | { kind: 'load'; carId: number; full: boolean } | { kind: 'service'; carId: number; inService: boolean };
// What a lobby display shows.interface CarStatus { readonly id: number; readonly floor: number; readonly direction: Direction; readonly doorsOpen: boolean;}
// The loop that owns the building. Node runs one callback at a time, so there is no data race// to prevent: a button handler can't run in the middle of tick(). The inbox is still worth// having, for the same reason as in the threaded languages: events are applied between ticks,// so the dispatcher always sees every car at rest, and a replay of the inbox replays the day.class Controller { private inbox: BuildingEvent[] = []; private status: readonly CarStatus[] = [];
constructor( private readonly system: ElevatorSystem, private readonly carCount: number, ) {}
post(e: BuildingEvent): void { this.inbox.push(e); }
tick(): void { const events = this.inbox; this.inbox = []; for (const e of events) { try { this.apply(e); } catch (err) { // A press for a floor that doesn't exist is dropped. An exception escaping a timer // callback crashes a Node process by default: one bad button would stop every car. if (!(err instanceof RangeError)) throw err; } } this.system.step(); this.status = Array.from({ length: this.carCount }, (_, id) => { const c = this.system.car(id); return { id, floor: c.floor, direction: c.direction, doorsOpen: c.doorsOpen }; }); }
// Turns one event into one call on the building. private apply(e: BuildingEvent): void { switch (e.kind) { case 'hall': return this.system.hallCall(e.floor, e.direction); case 'car': return this.system.carCall(e.carId, e.floor); case 'load': return this.system.setFull(e.carId, e.full); case 'service': return e.inService ? this.system.returnToService(e.carId) : this.system.takeOutOfService(e.carId); } }
snapshot(): readonly CarStatus[] { return this.status; }
// One timer, one tick at a time. Returns a function that stops it. start(periodMs: number): () => void { const timer = setInterval(() => this.tick(), periodMs); return () => clearInterval(timer); }}Three ideas hold it together:
- Two decisions, two owners. The order of stops belongs to each
Car: LOOK, fixed, and the reason nobody waits forever. The choice of car belongs to theDispatcher(Strategy): swappable, and the place to improve speed. Changing one never touches the other. - The system remembers every promise. A lit button is an entry in the call map or the waiting queue. Repeated presses, breakdowns and full cars are all handled by looking at that one record.
- Time and threads have one owner. Nothing moves except on a tick, and only the controller’s loop calls it. So no class below it needs a lock. Button presses become small event objects that wait in a queue (the Command pattern), and to the buttons, the system is a simple front door (a facade).
Stress it
Section titled “Stress it”The design works when one caller presses buttons and ticks the clock. But in a real building, buttons on thirty floors and inside four cars each report on their own thread.
Many buttons, one building
Section titled “Many buttons, one building”Buttons on thirty floors and inside four cars are all pressed at once, each on its own thread. Is your system safe?
Whether you pick one way of owning the state, and argue for it. Locks on every car plus a lock on the system leads to “now the dispatcher reads four cars: in what lock order?”. One owner thread with a queue is the strong answer, if you can say what it costs.
The danger isn’t one car’s fields getting mixed up. It’s decisions made on numbers that were never true together. The dispatcher reads the floor, direction and farthest stops of every car to choose one. If a car moves halfway through that read, the dispatcher compares a car’s old position with another car’s new one. And if a second press of the same button runs at the same moment, both presses find the button unlit, and the call is assigned twice.
The clearly correct fix is one lock around the whole system. Every press and every tick takes the same lock. That’s safe. But presses wait behind ticks, and every new method has to remember to take the lock.
The better fix is one owner. Buttons, load sensors and the maintenance panel never touch the
system directly. They put an event into a thread-safe queue, called the inbox, and return at
once. One control loop, on one thread, empties the inbox and then ticks. Because only that thread
ever touches ElevatorSystem, the cars, the call map and the dispatcher are all single-threaded
code. And every dispatcher decision sees every car standing still.
| Option | How | Correct? | Cost |
|---|---|---|---|
1. One lock around ElevatorSystem |
Every press and every tick takes it | Yes | Presses wait behind ticks; every new method must remember it |
| 2. A lock per car, plus one for the map | The dispatcher locks all cars to read them | Yes, with a fixed lock order | Threads can block each other forever if any path takes locks in another order |
| 3. One owner loop with an inbox (our choice) | Presses become events in a queue; one thread applies them and ticks | Yes, by design | A press waits up to one tick to take effect |
- Color key
- Facade
- Queue or stream
- Entity
// Everything that can happen to the building from outside, as values. Buttons, load sensors// and the maintenance panel run on their own threads; all they ever do is create one of these.sealed interface Event permits HallPressed, CarPressed, LoadChanged, ServiceChanged {}record HallPressed(int floor, Direction direction) implements Event {}record CarPressed(int carId, int floor) implements Event {}record LoadChanged(int carId, boolean full) implements Event {}record ServiceChanged(int carId, boolean inService) implements Event {}
// What a lobby display shows. Immutable, so any thread can read it without a lock.record CarStatus(int id, int floor, Direction direction, boolean doorsOpen) {}
// The one thread that owns the building. Other threads post events; only tick() touches// ElevatorSystem, so the cars, the dispatcher and the call map never need a lock.class Controller { private final ElevatorSystem system; private final int carCount; private final ConcurrentLinkedQueue<Event> inbox = new ConcurrentLinkedQueue<>(); // Replaced whole after every tick. volatile makes the new list visible to other threads. private volatile List<CarStatus> snapshot = List.of();
Controller(ElevatorSystem system, int carCount) { this.system = system; this.carCount = carCount; }
// Safe from any thread, and never blocks a button waiting for the loop. void post(Event e) { inbox.add(e); }
// Called by the control loop every tick, or directly by tests. Events are applied between // ticks, never in the middle of one, so the dispatcher always sees every car at rest. void tick() { for (Event e; (e = inbox.poll()) != null; ) { try { apply(e); } catch (IllegalArgumentException bad) { // A press for a floor that doesn't exist is dropped. Letting it escape would end // scheduleAtFixedRate's loop for good: one bad button would stop every car. } } system.step(); List<CarStatus> s = new ArrayList<>(); for (int i = 0; i < carCount; i++) { Car c = system.car(i); s.add(new CarStatus(i, c.floor(), c.direction(), c.doorsOpen())); } snapshot = List.copyOf(s); }
// Turns one event into one call on the building. private void apply(Event e) { switch (e) { case HallPressed h -> system.hallCall(h.floor(), h.direction()); case CarPressed c -> system.carCall(c.carId(), c.floor()); case LoadChanged l -> system.setFull(l.carId(), l.full()); case ServiceChanged s -> { if (s.inService()) system.returnToService(s.carId()); else system.takeOutOfService(s.carId()); } } }
List<CarStatus> snapshot() { return snapshot; }
// One thread at a fixed rate. The executor never runs two ticks at once, which is the // whole concurrency design: there is exactly one writer. ScheduledFuture<?> start(ScheduledExecutorService exec, long periodMs) { return exec.scheduleAtFixedRate(this::tick, 0, periodMs, TimeUnit.MILLISECONDS); }}import queueimport threadingfrom dataclasses import dataclass
# Everything that can happen to the building from outside, as values. Buttons, load sensors# and the maintenance panel run on their own threads; all they ever do is create one of these.@dataclass(frozen=True)class HallPressed: floor: int direction: Direction
@dataclass(frozen=True)class CarPressed: car_id: int floor: int
@dataclass(frozen=True)class LoadChanged: car_id: int full: bool
@dataclass(frozen=True)class ServiceChanged: car_id: int in_service: bool
Event = HallPressed | CarPressed | LoadChanged | ServiceChanged
# What a lobby display shows. Immutable, so any thread can read it without a lock.@dataclass(frozen=True)class CarStatus: id: int floor: int direction: Direction doors_open: bool
# The one thread that owns the building. Other threads post events; only tick() touches# ElevatorSystem, so the cars, the dispatcher and the call map never need a lock.class Controller: def __init__(self, system: ElevatorSystem, car_count: int): self._system = system self._car_count = car_count self._inbox: queue.SimpleQueue[Event] = queue.SimpleQueue() # thread-safe, never blocks a put # Replaced whole after every tick. Rebinding one attribute is atomic in CPython, # so a reader gets the old tuple or the new one, never half of each. self.snapshot: tuple[CarStatus, ...] = ()
# Safe from any thread, and never blocks a button waiting for the loop. def post(self, e: Event) -> None: self._inbox.put(e)
# Called by the control loop every tick, or directly by tests. Events are applied between # ticks, never in the middle of one, so the dispatcher always sees every car at rest. def tick(self) -> None: while True: try: e = self._inbox.get_nowait() except queue.Empty: break try: self._apply(e) except ValueError: pass # a press for a floor that doesn't exist is dropped, not allowed to stop the loop self._system.step() self.snapshot = tuple( CarStatus(i, c.floor, c.direction, c.doors_open) for i in range(self._car_count) for c in [self._system.car(i)] )
# Turns one event into one call on the building. def _apply(self, e: Event) -> None: match e: case HallPressed(floor, direction): self._system.hall_call(floor, direction) case CarPressed(car_id, floor): self._system.car_call(car_id, floor) case LoadChanged(car_id, full): self._system.set_full(car_id, full) case ServiceChanged(car_id, True): self._system.return_to_service(car_id) case ServiceChanged(car_id, False): self._system.take_out_of_service(car_id)
# One thread at a fixed rate: there is exactly one writer, which is the whole design. def run(self, period_s: float, stop: threading.Event) -> None: while not stop.wait(period_s): self.tick()// Everything that can happen to the building from outside, as values. Buttons, load sensors// and the maintenance panel run on their own threads; all they ever do is create one of these.public abstract record BuildingEvent;public sealed record HallPressed(int Floor, Direction Direction) : BuildingEvent;public sealed record CarPressed(int CarId, int Floor) : BuildingEvent;public sealed record LoadChanged(int CarId, bool Full) : BuildingEvent;public sealed record ServiceChanged(int CarId, bool InService) : BuildingEvent;
// What a lobby display shows. Immutable, so any thread can read it without a lock.public sealed record CarStatus(int Id, int Floor, Direction Direction, bool DoorsOpen);
// The one thread that owns the building. Other threads post events; only Tick() touches// ElevatorSystem, so the cars, the dispatcher and the call map never need a lock.public sealed class Controller(ElevatorSystem system, int carCount){ private readonly ConcurrentQueue<BuildingEvent> _inbox = new(); // Replaced whole after every tick. volatile makes the new list visible to other threads. private volatile IReadOnlyList<CarStatus> _snapshot = [];
// Safe from any thread, and never blocks a button waiting for the loop. public void Post(BuildingEvent e) => _inbox.Enqueue(e);
// Called by the control loop every tick, or directly by tests. Events are applied between // ticks, never in the middle of one, so the dispatcher always sees every car at rest. public void Tick() { while (_inbox.TryDequeue(out var e)) { try { Apply(e); } catch (ArgumentOutOfRangeException) { // A press for a floor that doesn't exist is dropped. Letting it escape would end // the loop below: one bad button would stop every car. } } system.Step(); _snapshot = Enumerable.Range(0, carCount) .Select(i => system.Car(i)) .Select(c => new CarStatus(c.Id, c.Floor, c.Direction, c.DoorsOpen)) .ToList(); }
// Turns one event into one call on the building. private void Apply(BuildingEvent e) { switch (e) { case HallPressed h: system.HallCall(h.Floor, h.Direction); break; case CarPressed c: system.CarCall(c.CarId, c.Floor); break; case LoadChanged l: system.SetFull(l.CarId, l.Full); break; case ServiceChanged { InService: true } s: system.ReturnToService(s.CarId); break; case ServiceChanged s: system.TakeOutOfService(s.CarId); break; } }
public IReadOnlyList<CarStatus> Snapshot => _snapshot;
// One loop, one tick at a time: PeriodicTimer never overlaps ticks, which is the whole // concurrency design. There is exactly one writer. public async Task RunAsync(TimeSpan period, CancellationToken stop) { using var timer = new PeriodicTimer(period); while (await timer.WaitForNextTickAsync(stop)) Tick(); }}// Event is everything that can happen to the building from outside, as values. Buttons, load// sensors and the maintenance panel run on their own goroutines; all they do is send one.type Event interface{ isEvent() }
type HallPressed struct { Floor int Direction Direction}type CarPressed struct{ CarID, Floor int }type LoadChanged struct { CarID int Full bool}type ServiceChanged struct { CarID int InService bool}
func (HallPressed) isEvent() {}func (CarPressed) isEvent() {}func (LoadChanged) isEvent() {}func (ServiceChanged) isEvent() {}
// CarStatus is what a lobby display shows. A copy, so any goroutine can keep it.type CarStatus struct { ID, Floor int Direction Direction DoorsOpen bool}
// Controller is the one goroutine that owns the building. Others send events on a channel;// only Tick touches the ElevatorSystem, so the cars, the dispatcher and the call map never// need a mutex. "Share memory by communicating."type Controller struct { system *ElevatorSystem carCount int Inbox chan Event // buffered, so a button rarely waits snapshot atomic.Pointer[[]CarStatus] // replaced whole after every tick}
func NewController(s *ElevatorSystem, carCount int) *Controller { c := &Controller{system: s, carCount: carCount, Inbox: make(chan Event, 256)} c.snapshot.Store(&[]CarStatus{}) return c}
// Tick applies every event that has arrived, then moves the cars one step. Events land// between ticks, never in the middle of one, so the dispatcher always sees every car at rest.func (c *Controller) Tick() { for drained := false; !drained; { select { case e := <-c.Inbox: // A press for a floor that doesn't exist comes back as an error and is dropped. _ = c.apply(e) default: drained = true } } c.system.Step() s := make([]CarStatus, c.carCount) for i := range s { car := c.system.Car(i) s[i] = CarStatus{i, car.Floor, car.Direction, car.DoorsOpen} } c.snapshot.Store(&s)}
// apply turns one event into one call on the building.func (c *Controller) apply(e Event) error { switch e := e.(type) { case HallPressed: return c.system.HallCall(e.Floor, e.Direction) case CarPressed: return c.system.CarCall(e.CarID, e.Floor) case LoadChanged: c.system.SetFull(e.CarID, e.Full) case ServiceChanged: if e.InService { c.system.ReturnToService(e.CarID) } else { c.system.TakeOutOfService(e.CarID) } } return nil}
func (c *Controller) Snapshot() []CarStatus { return *c.snapshot.Load() }
// Run ticks at a fixed rate until stop is closed. One goroutine, one writer: that is the// whole concurrency design.func (c *Controller) Run(period time.Duration, stop <-chan struct{}) { t := time.NewTicker(period) defer t.Stop() for { select { case <-t.C: c.Tick() case <-stop: return } }}// Everything that can happen to the building from outside, as values.type BuildingEvent = | { kind: 'hall'; floor: number; direction: 'up' | 'down' } | { kind: 'car'; carId: number; floor: number } | { kind: 'load'; carId: number; full: boolean } | { kind: 'service'; carId: number; inService: boolean };
// What a lobby display shows.interface CarStatus { readonly id: number; readonly floor: number; readonly direction: Direction; readonly doorsOpen: boolean;}
// The loop that owns the building. Node runs one callback at a time, so there is no data race// to prevent: a button handler can't run in the middle of tick(). The inbox is still worth// having, for the same reason as in the threaded languages: events are applied between ticks,// so the dispatcher always sees every car at rest, and a replay of the inbox replays the day.class Controller { private inbox: BuildingEvent[] = []; private status: readonly CarStatus[] = [];
constructor( private readonly system: ElevatorSystem, private readonly carCount: number, ) {}
post(e: BuildingEvent): void { this.inbox.push(e); }
tick(): void { const events = this.inbox; this.inbox = []; for (const e of events) { try { this.apply(e); } catch (err) { // A press for a floor that doesn't exist is dropped. An exception escaping a timer // callback crashes a Node process by default: one bad button would stop every car. if (!(err instanceof RangeError)) throw err; } } this.system.step(); this.status = Array.from({ length: this.carCount }, (_, id) => { const c = this.system.car(id); return { id, floor: c.floor, direction: c.direction, doorsOpen: c.doorsOpen }; }); }
// Turns one event into one call on the building. private apply(e: BuildingEvent): void { switch (e.kind) { case 'hall': return this.system.hallCall(e.floor, e.direction); case 'car': return this.system.carCall(e.carId, e.floor); case 'load': return this.system.setFull(e.carId, e.full); case 'service': return e.inService ? this.system.returnToService(e.carId) : this.system.takeOutOfService(e.carId); } }
snapshot(): readonly CarStatus[] { return this.status; }
// One timer, one tick at a time. Returns a function that stops it. start(periodMs: number): () => void { const timer = setInterval(() => this.tick(), periodMs); return () => clearInterval(timer); }}Why is this correct? Exactly one thread reads or writes the system, so nothing below the controller can race. The inbox is the only shared object, and it’s a library queue built for many writers and one reader. Screens read a small status snapshot that’s built after each tick and replaced whole. A reader gets the old snapshot or the new one, never a mix.
What does it cost? A press takes effect on the next tick, not instantly, and all the work runs on one thread. For an elevator bank, that’s nothing: a few cars, and a tick every few hundred milliseconds. No rider notices half a second. A system with thousands of independent units would split them across several loops.
Go deeperTwo details that matter in production1 min
A bad event must not stop the loop. A press for a floor that doesn’t exist throws an error
inside the loop. In Java, an error escaping a scheduleAtFixedRate task cancels every future run.
In Node, an uncaught error in a timer crashes the process. Either way, one faulty button would stop
every car. So the controller drops invalid events and keeps going.
The tick must be short. If a tick waited on a disk or the network, every press would wait behind it. Go uses a channel as the inbox and a ticker, which is the same design in Go’s own style: “share memory by communicating”.
Go deeperWhat if the controller crashes and restarts?1 min
The state that matters is small: each car’s position (read back from the hardware’s floor sensors) and the lit buttons. A restarted controller rebuilds the cars from the sensor readings, and treats every lit button as a fresh floor call. Riders inside press their floor again if the car stops. Saving the inbox would allow an exact replay, but rebuilding from sensors is simpler, and it’s what the hardware already supports.
How you’d prove it works
Section titled “How you’d prove it works”Because time is a method, every test is a script of presses and ticks. The ones that matter:
- A car visits stops in sweep order, not press order.
- A car going up passes a down call, and takes it on the way back.
- A down call at the top of a sweep turns the car around.
- The dispatcher skips a near car that’s heading away.
- No call waits forever under heavy traffic.
- A broken car’s calls move to another car, and the button goes dark when answered.
- Pressing a lit button again isn’t a new call.
- Events sent from many threads are all applied, and a bad one doesn’t stop the tick.
The starvation test protects the main promise. Riders keep asking for floors 1 and 2 on every tick, while one person waits on floor 19. A LOOK car reaches 19 within a couple of sweeps. If someone “improves” the car to nearest-first, the test fails, because the car never leaves the bottom of the building.
Go deeperThe tests in five languages2 min
The many-threads test sends a hundred presses from eight threads, and checks that every button is lit after a single tick.
// Steps a car until it is idle, and returns the floors where its doors opened, in order.static List<Integer> stops(Car car, int maxTicks) { List<Integer> opened = new ArrayList<>(); for (int t = 0; t < maxTicks; t++) { car.step(); if (car.doorsOpen()) opened.add(car.floor()); } return opened;}
@Testvoid aCarVisitsStopsInSweepOrderNotPressOrder() { Car car = new Car(0, 10); car.carCall(5); car.carCall(2); car.carCall(8); assertEquals(List.of(2, 5, 8), stops(car, 30)); // first come, first served would be 5, 2, 8}
@Testvoid aCarGoingUpPassesADownCallAndTakesItOnTheWayBack() { Car car = new Car(0, 10); car.carCall(9); car.hallCall(new HallCall(5, Direction.DOWN)); assertEquals(List.of(9, 5), stops(car, 30)); // the person on 5 wants down, so not on the way up}
@Testvoid aDownCallAtTheTopTurnsTheCarAround() { Car car = new Car(0, 10); car.hallCall(new HallCall(8, Direction.DOWN)); stops(car, 9); // 8 floors up, then the doors open assertTrue(car.doorsOpen()); assertEquals(Direction.DOWN, car.direction()); // the lantern shows the way they are going}
@Testvoid theDispatcherSkipsANearCarThatIsHeadingAway() { ElevatorSystem s = new ElevatorSystem(20, 2, new NearestCarDispatcher()); s.carCall(0, 19); // car 0 heads for the top s.carCall(1, 6); // car 1 stops at 6 and goes idle for (int t = 0; t < 12; t++) s.step(); // car 0 on 12 going up, car 1 idle on 6 Car pick = new NearestCarDispatcher().choose(List.of(s.car(0), s.car(1)), new HallCall(11, Direction.UP)); assertEquals(1, pick.id); // car 0 is one floor away, but 15 floors from arriving}
@Testvoid noCallWaitsForeverUnderHeavyTraffic() { // Riders keep asking for 1 and 2. A nearest-stop-first car would never leave the bottom. Car car = new Car(0, 20); car.hallCall(new HallCall(19, Direction.DOWN)); for (int t = 0; t < 60; t++) { int f = t % 2 + 1; if (car.floor() != f) car.carCall(f); car.step(); if (car.floor() == 19 && car.doorsOpen()) return; } throw new AssertionError("the call on 19 was never answered");}
@Testvoid aBrokenCarsCallsMoveToAnotherCar() { ElevatorSystem s = new ElevatorSystem(10, 2, new NearestCarDispatcher()); s.hallCall(7, Direction.DOWN); // both idle on 0: the tie goes to car 0 s.takeOutOfService(0); for (int t = 0; t < 10 && !(s.car(1).floor() == 7 && s.car(1).doorsOpen()); t++) s.step(); assertEquals(7, s.car(1).floor()); assertFalse(s.isLit(new HallCall(7, Direction.DOWN)));}
@Testvoid pressingALitButtonAgainIsNotANewCall() { int[] chosen = {0}; Dispatcher counting = (cars, call) -> { chosen[0]++; return cars.get(0); }; ElevatorSystem s = new ElevatorSystem(10, 2, counting); for (int i = 0; i < 5; i++) s.hallCall(4, Direction.UP); assertEquals(1, chosen[0]);}
@Testvoid eventsPostedFromManyThreadsAreAllApplied() throws Exception { ElevatorSystem system = new ElevatorSystem(50, 3, new NearestCarDispatcher()); Controller c = new Controller(system, 3); ExecutorService buttons = Executors.newFixedThreadPool(8); for (int f = 0; f < 50; f++) { int floor = f; buttons.submit(() -> c.post(new HallPressed(floor, Direction.UP))); buttons.submit(() -> c.post(new HallPressed(floor, Direction.DOWN))); } buttons.shutdown(); buttons.awaitTermination(5, TimeUnit.SECONDS); c.post(new CarPressed(0, 99)); // a floor that doesn't exist: dropped, not fatal c.tick(); assertEquals(3, c.snapshot().size()); // the tick finished despite the bad press // Every press landed. The ones on floor 0 were answered in the same tick by the cars there. for (int f = 1; f < 50; f++) { assertTrue(system.isLit(new HallCall(f, Direction.UP))); assertTrue(system.isLit(new HallCall(f, Direction.DOWN))); }}from concurrent.futures import ThreadPoolExecutor
# Steps a car for a number of ticks and returns the floors where its doors opened, in order.def stops(car: Car, ticks: int) -> list[int]: opened = [] for _ in range(ticks): car.step() if car.doors_open: opened.append(car.floor) return opened
def test_a_car_visits_stops_in_sweep_order_not_press_order(): car = Car(0, 10) for f in (5, 2, 8): car.car_call(f) assert stops(car, 30) == [2, 5, 8] # first come, first served would be 5, 2, 8
def test_a_car_going_up_passes_a_down_call_and_takes_it_on_the_way_back(): car = Car(0, 10) car.car_call(9) car.hall_call(HallCall(5, Direction.DOWN)) assert stops(car, 30) == [9, 5] # the person on 5 wants down, so not on the way up
def test_a_down_call_at_the_top_turns_the_car_around(): car = Car(0, 10) car.hall_call(HallCall(8, Direction.DOWN)) stops(car, 9) # 8 floors up, then the doors open assert car.doors_open assert car.direction is Direction.DOWN # the lantern shows the way they are going
def test_the_dispatcher_skips_a_near_car_that_is_heading_away(): s = ElevatorSystem(20, 2, NearestCarDispatcher()) s.car_call(0, 19) # car 0 heads for the top s.car_call(1, 6) # car 1 stops at 6 and goes idle for _ in range(12): s.step() # car 0 on 12 going up, car 1 idle on 6 pick = NearestCarDispatcher().choose([s.car(0), s.car(1)], HallCall(11, Direction.UP)) assert pick.id == 1 # car 0 is one floor away, but 15 floors from arriving
def test_no_call_waits_forever_under_heavy_traffic(): # Riders keep asking for 1 and 2. A nearest-stop-first car would never leave the bottom. car = Car(0, 20) car.hall_call(HallCall(19, Direction.DOWN)) for t in range(60): f = t % 2 + 1 if car.floor != f: car.car_call(f) car.step() if car.floor == 19 and car.doors_open: return raise AssertionError("the call on 19 was never answered")
def test_a_broken_cars_calls_move_to_another_car(): s = ElevatorSystem(10, 2, NearestCarDispatcher()) s.hall_call(7, Direction.DOWN) # both idle on 0: the tie goes to car 0 s.take_out_of_service(0) for _ in range(10): s.step() if s.car(1).floor == 7 and s.car(1).doors_open: break assert s.car(1).floor == 7 assert not s.is_lit(HallCall(7, Direction.DOWN))
def test_pressing_a_lit_button_again_is_not_a_new_call(): chosen = []
class Counting: def choose(self, cars, call): chosen.append(call) return cars[0]
s = ElevatorSystem(10, 2, Counting()) for _ in range(5): s.hall_call(4, Direction.UP) assert len(chosen) == 1
def test_events_posted_from_many_threads_are_all_applied(): system = ElevatorSystem(50, 3, NearestCarDispatcher()) c = Controller(system, 3) presses = [HallPressed(f, d) for f in range(50) for d in (Direction.UP, Direction.DOWN)] with ThreadPoolExecutor(max_workers=8) as buttons: list(buttons.map(c.post, presses)) c.post(CarPressed(0, 99)) # a floor that doesn't exist: dropped, not fatal c.tick() assert len(c.snapshot) == 3 # the tick finished despite the bad press # Every press landed. The ones on floor 0 were answered in the same tick by the cars there. assert all(system.is_lit(HallCall(p.floor, p.direction)) or p.floor == 0 for p in presses)// Steps a car for a number of ticks and returns the floors where its doors opened, in order.private static List<int> Stops(Car car, int ticks){ var opened = new List<int>(); for (int t = 0; t < ticks; t++) { car.Step(); if (car.DoorsOpen) opened.Add(car.Floor); } return opened;}
[Fact]public void ACarVisitsStopsInSweepOrderNotPressOrder(){ var car = new Car(0, 10); car.CarCall(5); car.CarCall(2); car.CarCall(8); Assert.Equal(new[] { 2, 5, 8 }, Stops(car, 30)); // first come, first served would be 5, 2, 8}
[Fact]public void ACarGoingUpPassesADownCallAndTakesItOnTheWayBack(){ var car = new Car(0, 10); car.CarCall(9); car.HallCall(new HallCall(5, Direction.Down)); Assert.Equal(new[] { 9, 5 }, Stops(car, 30)); // the person on 5 wants down, so not on the way up}
[Fact]public void ADownCallAtTheTopTurnsTheCarAround(){ var car = new Car(0, 10); car.HallCall(new HallCall(8, Direction.Down)); Stops(car, 9); // 8 floors up, then the doors open Assert.True(car.DoorsOpen); Assert.Equal(Direction.Down, car.Direction); // the lantern shows the way they are going}
[Fact]public void TheDispatcherSkipsANearCarThatIsHeadingAway(){ var s = new ElevatorSystem(20, 2, new NearestCarDispatcher()); s.CarCall(0, 19); // car 0 heads for the top s.CarCall(1, 6); // car 1 stops at 6 and goes idle for (int t = 0; t < 12; t++) s.Step(); // car 0 on 12 going up, car 1 idle on 6 var pick = new NearestCarDispatcher().Choose([s.Car(0), s.Car(1)], new HallCall(11, Direction.Up)); Assert.Equal(1, pick.Id); // car 0 is one floor away, but 15 floors from arriving}
[Fact]public void NoCallWaitsForeverUnderHeavyTraffic(){ // Riders keep asking for 1 and 2. A nearest-stop-first car would never leave the bottom. var car = new Car(0, 20); car.HallCall(new HallCall(19, Direction.Down)); for (int t = 0; t < 60; t++) { int f = t % 2 + 1; if (car.Floor != f) car.CarCall(f); car.Step(); if (car.Floor == 19 && car.DoorsOpen) return; } Assert.Fail("the call on 19 was never answered");}
[Fact]public void ABrokenCarsCallsMoveToAnotherCar(){ var s = new ElevatorSystem(10, 2, new NearestCarDispatcher()); s.HallCall(7, Direction.Down); // both idle on 0: the tie goes to car 0 s.TakeOutOfService(0); for (int t = 0; t < 10 && !(s.Car(1).Floor == 7 && s.Car(1).DoorsOpen); t++) s.Step(); Assert.Equal(7, s.Car(1).Floor); Assert.False(s.IsLit(new HallCall(7, Direction.Down)));}
private sealed class CountingDispatcher : IDispatcher{ public int Calls; public Car Choose(IReadOnlyList<Car> cars, HallCall call) { Calls++; return cars[0]; }}
[Fact]public void PressingALitButtonAgainIsNotANewCall(){ var d = new CountingDispatcher(); var s = new ElevatorSystem(10, 2, d); for (int i = 0; i < 5; i++) s.HallCall(4, Direction.Up); Assert.Equal(1, d.Calls);}
[Fact]public void EventsPostedFromManyThreadsAreAllApplied(){ var system = new ElevatorSystem(50, 3, new NearestCarDispatcher()); var c = new Controller(system, 3); Parallel.For(0, 50, f => { c.Post(new HallPressed(f, Direction.Up)); c.Post(new HallPressed(f, Direction.Down)); }); c.Post(new CarPressed(0, 99)); // a floor that doesn't exist: dropped, not fatal c.Tick(); Assert.Equal(3, c.Snapshot.Count); // the tick finished despite the bad press // Every press landed. The ones on floor 0 were answered in the same tick by the cars there. for (int f = 1; f < 50; f++) { Assert.True(system.IsLit(new HallCall(f, Direction.Up))); Assert.True(system.IsLit(new HallCall(f, Direction.Down))); }}// stops steps a car for a number of ticks and returns the floors where its doors opened.func stops(c *Car, ticks int) []int { var opened []int for range ticks { c.Step() if c.DoorsOpen { opened = append(opened, c.Floor) } } return opened}
func TestACarVisitsStopsInSweepOrderNotPressOrder(t *testing.T) { c := NewCar(0, 10) for _, f := range []int{5, 2, 8} { c.CarCall(f) } if got := stops(c, 30); !slices.Equal(got, []int{2, 5, 8}) { // first come, first served would be 5, 2, 8 t.Fatalf("stopped at %v", got) }}
func TestACarGoingUpPassesADownCallAndTakesItOnTheWayBack(t *testing.T) { c := NewCar(0, 10) c.CarCall(9) c.HallCall(HallCall{5, Down}) if got := stops(c, 30); !slices.Equal(got, []int{9, 5}) { // the person on 5 wants down t.Fatalf("stopped at %v", got) }}
func TestADownCallAtTheTopTurnsTheCarAround(t *testing.T) { c := NewCar(0, 10) c.HallCall(HallCall{8, Down}) stops(c, 9) // 8 floors up, then the doors open if !c.DoorsOpen || c.Direction != Down { t.Fatalf("doors %v, direction %v", c.DoorsOpen, c.Direction) }}
func TestTheDispatcherSkipsANearCarThatIsHeadingAway(t *testing.T) { s := NewElevatorSystem(20, 2, NearestCar{}) s.CarCall(0, 19) // car 0 heads for the top s.CarCall(1, 6) // car 1 stops at 6 and goes idle for range 12 { s.Step() // car 0 on 12 going up, car 1 idle on 6 } pick := NearestCar{}.Choose([]*Car{s.Car(0), s.Car(1)}, HallCall{11, Up}) if pick.ID != 1 { // car 0 is one floor away, but 15 floors from arriving t.Fatalf("picked car %d", pick.ID) }}
func TestNoCallWaitsForeverUnderHeavyTraffic(t *testing.T) { // Riders keep asking for 1 and 2. A nearest-stop-first car would never leave the bottom. c := NewCar(0, 20) c.HallCall(HallCall{19, Down}) for i := range 60 { if f := i%2 + 1; c.Floor != f { c.CarCall(f) } c.Step() if c.Floor == 19 && c.DoorsOpen { return } } t.Fatal("the call on 19 was never answered")}
func TestABrokenCarsCallsMoveToAnotherCar(t *testing.T) { s := NewElevatorSystem(10, 2, NearestCar{}) s.HallCall(7, Down) // both idle on 0: the tie goes to car 0 s.TakeOutOfService(0) for range 10 { s.Step() if s.Car(1).Floor == 7 && s.Car(1).DoorsOpen { break } } if s.Car(1).Floor != 7 || s.IsLit(HallCall{7, Down}) { t.Fatalf("car 1 on %d, still lit %v", s.Car(1).Floor, s.IsLit(HallCall{7, Down})) }}
type countingDispatcher struct{ calls int }
func (d *countingDispatcher) Choose(cars []*Car, _ HallCall) *Car { d.calls++ return cars[0]}
func TestPressingALitButtonAgainIsNotANewCall(t *testing.T) { d := &countingDispatcher{} s := NewElevatorSystem(10, 2, d) for range 5 { s.HallCall(4, Up) } if d.calls != 1 { t.Fatalf("dispatched %d times", d.calls) }}
// Run with: go test -race ./... The race detector flags any state touched off the loop.func TestEventsSentFromManyGoroutinesAreAllApplied(t *testing.T) { s := NewElevatorSystem(50, 3, NearestCar{}) c := NewController(s, 3) var buttons sync.WaitGroup for f := range 50 { buttons.Add(1) go func() { defer buttons.Done() c.Inbox <- HallPressed{f, Up} c.Inbox <- HallPressed{f, Down} }() } buttons.Wait() c.Inbox <- CarPressed{0, 99} // a floor that doesn't exist: dropped, not fatal c.Tick() if len(c.Snapshot()) != 3 { t.Fatal("tick did not finish") } // Every press landed. The ones on floor 0 were answered in the same tick by the cars there. for f := 1; f < 50; f++ { if !s.IsLit(HallCall{f, Up}) || !s.IsLit(HallCall{f, Down}) { t.Fatalf("floor %d lost a press", f) } }}import { test } from 'node:test';import assert from 'node:assert/strict';
// Steps a car for a number of ticks and returns the floors where its doors opened, in order.function stops(car: Car, ticks: number): number[] { const opened: number[] = []; for (let t = 0; t < ticks; t++) { car.step(); if (car.doorsOpen) opened.push(car.floor); } return opened;}
test('a car visits stops in sweep order, not press order', () => { const car = new Car(0, 10); for (const f of [5, 2, 8]) car.carCall(f); assert.deepEqual(stops(car, 30), [2, 5, 8]); // first come, first served would be 5, 2, 8});
test('a car going up passes a down call and takes it on the way back', () => { const car = new Car(0, 10); car.carCall(9); car.hallCall({ floor: 5, direction: 'down' }); assert.deepEqual(stops(car, 30), [9, 5]); // the person on 5 wants down, so not on the way up});
test('a down call at the top turns the car around', () => { const car = new Car(0, 10); car.hallCall({ floor: 8, direction: 'down' }); stops(car, 9); // 8 floors up, then the doors open assert.ok(car.doorsOpen); assert.equal(car.direction, 'down'); // the lantern shows the way they are going});
test('the dispatcher skips a near car that is heading away', () => { const s = new ElevatorSystem(20, 2, new NearestCarDispatcher()); s.carCall(0, 19); // car 0 heads for the top s.carCall(1, 6); // car 1 stops at 6 and goes idle for (let t = 0; t < 12; t++) s.step(); // car 0 on 12 going up, car 1 idle on 6 const pick = new NearestCarDispatcher().choose([s.car(0), s.car(1)], { floor: 11, direction: 'up' }); assert.equal(pick.id, 1); // car 0 is one floor away, but 15 floors from arriving});
test('no call waits forever under heavy traffic', () => { // Riders keep asking for 1 and 2. A nearest-stop-first car would never leave the bottom. const car = new Car(0, 20); car.hallCall({ floor: 19, direction: 'down' }); for (let t = 0; t < 60; t++) { const f = (t % 2) + 1; if (car.floor !== f) car.carCall(f); car.step(); if (car.floor === 19 && car.doorsOpen) return; } assert.fail('the call on 19 was never answered');});
test("a broken car's calls move to another car", () => { const s = new ElevatorSystem(10, 2, new NearestCarDispatcher()); s.hallCall(7, 'down'); // both idle on 0: the tie goes to car 0 s.takeOutOfService(0); for (let t = 0; t < 10 && !(s.car(1).floor === 7 && s.car(1).doorsOpen); t++) s.step(); assert.equal(s.car(1).floor, 7); assert.ok(!s.isLit({ floor: 7, direction: 'down' }));});
test('pressing a lit button again is not a new call', () => { let chosen = 0; const counting: Dispatcher = { choose: (cars) => { chosen++; return cars[0]; }, }; const s = new ElevatorSystem(10, 2, counting); for (let i = 0; i < 5; i++) s.hallCall(4, 'up'); assert.equal(chosen, 1);});
test('a bad press is dropped and the rest of the inbox still lands', () => { const system = new ElevatorSystem(10, 2, new NearestCarDispatcher()); const c = new Controller(system, 2); c.post({ kind: 'car', carId: 0, floor: 99 }); // a floor that doesn't exist c.post({ kind: 'hall', floor: 6, direction: 'up' }); c.tick(); assert.equal(c.snapshot().length, 2); assert.ok(system.isLit({ floor: 6, direction: 'up' }));});Go deeperEvery edge case, and where it is handled2 min
| Case | What happens | Where |
|---|---|---|
| Rider presses the floor the car is on | Doors open on the next tick | Car.step |
| Down call at the top of a sweep | Taken at the turnaround; the car’s direction becomes down | Car.serveHere |
| Up call below a car going up | Served on the next up sweep | LOOK in Car.nextDirection |
| Lit button pressed again | No new call | ElevatorSystem.hallCall |
| Car taken out of service holding calls | Calls handed back and reassigned | takeOutOfService |
| Every car full or out of service | The call waits and is offered again every tick | ElevatorSystem.step |
| Full car with calls already assigned | Keeps them; riders may get off before it arrives | Car.accepting filters new ones only |
| Car returns to service | Doors close next tick; it can be chosen again | returnToService |
| Press for a floor that doesn’t exist | Dropped; the loop keeps running | Controller.tick |
| Two presses of one button in one tick | The second finds the call lit | the call map |
| Screen reads during a tick | Sees the previous snapshot, whole | unchanging CarStatus list |
Wrap up
Section titled “Wrap up”The decisions, and what each one costs
Section titled “The decisions, and what each one costs”| Decision | We chose | We gave up |
|---|---|---|
| Time | An explicit step() tick, called by the owner |
Modeling motors and doors in real time |
| Stop order in a car | LOOK, with car stops, up calls and down calls | The shortest possible travel at every moment |
| Floor calls | A value with a floor and a direction | Nothing; a floor alone is wrong |
| Which car answers | Strategy (Dispatcher), cost counted to arrival |
An estimate that ignores stops along the way |
| When the choice is made | Once, when pressed | Better choices from reassigning every tick |
| Broken and full cars | A record of every accepted call; broken cars hand theirs back | One more map to keep in step |
| Special modes | Flags for now | State objects once modes multiply |
| Concurrency | One owner loop with an inbox of events | Up to one tick of delay per press |
Go deeperGenuinely different designs, and when to pick them2 min
A thread per car, in real time. Each car runs its own loop, sleeping between floors, with a locked list of stops. It mirrors the physical building, and many tutorials show it. It’s harder to test, needs locking between the dispatcher and the cars, and gains nothing, because cars don’t need to run in parallel. Pick it only if each car’s controller runs on separate hardware. Then it’s a distributed system with messages, not threads.
Choosing the best plan on every tick. Instead of choosing a car once per call, work out the
best assignment of all waiting calls to all cars on every tick, for example to make the longest
waits as short as possible. This is roughly what modern controllers do, with clever shortcuts. It
gives better averages, but calls can move between cars, so the lantern over the doors can’t be lit
early. It fits in as a dispatcher that runs from ElevatorSystem.step.
A simulation that jumps between events. Instead of fixed ticks, jump the clock straight to the next interesting moment (a car arriving, doors closing). It’s far faster for simulating a whole day to compare dispatchers. For the controller itself, fixed ticks are simpler and match how the hardware reports.
How to spend 45 minutes
Section titled “How to spend 45 minutes”- 0 to 3. Floors and cars, two buttons or a keypad, and what “good” means: nobody waits forever first.
- 3 to 5. The promises. Name “no call waits forever” as the hard one.
- 5 to 25. One car with ticks, then floor calls with direction and LOOK, then the dispatcher,
then broken and full cars with the call map. Code
Car.stepand the dispatcher’s cost, and describe the call map. - 25 to 28. Walk through the full diagram: two decisions with two owners, one record of promises.
- 28 to 36. Concurrency: the inbox and the single owner loop, and why not locks per car.
- 36 to 45. Destination dispatch or rush-hour modes, then your own questions.
If time is short, describe stage 4 in words. Never skip the starvation argument for LOOK. It’s the reason the design is what it is.
Follow-up questions
Section titled “Follow-up questions”The building wants destination dispatch: riders type their floor in the lobby, and the screen tells them which car to take. What changes?
Whether you see that the request now carries both ends of the trip, so the dispatcher can group riders going to nearby floors, and that the car’s sweep doesn’t change at all.
Most extensions fit into one place:
| Extension | What changes | Touches Car? |
|---|---|---|
| Destination dispatch | A TripRequest(from, to); a dispatcher that groups trips; the car gets a floor call and a car stop |
No |
| Morning rush | A second dispatcher that parks idle cars at the lobby, switched by time of day | No |
| Zoned or express cars | The system filters cars by which floors they serve | No |
| Fire service | A mode that sends every car to the lobby and ignores floor calls | Yes: mode as State |
| Reassign stale calls | The system re-runs the dispatcher for waiting calls every few ticks | No |
| Energy saving | A dispatcher whose cost adds a penalty for starting an idle car | No |
Go deeperAdding destination dispatch, step by step2 min
With up and down buttons, the system learns where a rider is going only after they step into the car. With a keypad in the lobby, it knows both ends of the trip before choosing a car. That’s the whole change, and it lands in the request and the dispatcher.
- A new request.
TripRequest(from, to)replaces “up on floor 1”. The direction isn’t needed any more: it follows from the two floors. - A new dispatcher. It groups riders going to nearby floors into the same car. Five people going to floors 20 to 23 share one car, which then makes four stops instead of each car making one or two. The cost function adds “how many new stops does this trip add to this car?” to the arrival time we already had.
- The answer goes back to the rider. The keypad shows “take car B”. So the choice must stick, which we already do: a car is chosen once, when the request arrives.
- The car gets two ordinary stops. The system turns one trip into a floor call at
from(in the direction ofto) and a car stop atto. The car doesn’t know destination dispatch exists. Its sweep, and the promise that nobody waits forever, are unchanged.
One new edge case appears: a rider who types a floor and then gets into the wrong car. The car stop
at to is already set on the right car, so the wrong car doesn’t know where they want to go. Most
buildings answer this with a keypad inside each car too, so a rider can still press their floor.
Go deeperFour more follow-up questions2 min
Interviewer asksHow would you measure whether a new dispatcher is better?
Simulate. Because time is a tick and every input is an event, a script can replay a recorded day of button presses against two dispatchers, and compare the average wait, the 95th percentile wait, and the longest wait. The longest wait matters most: a dispatcher that improves the average by leaving one floor waiting is worse.
Interviewer asksTwo floor calls on the same floor, one up and one down. Same car?
Not necessarily. They’re different calls with different directions, and the dispatcher picks the best car for each. Often the same car takes both, one on each sweep. The call map uses floor and direction together as its key, so they never clash.
Interviewer asksWhere does the 'door open' button inside the car fit?
It’s one more event. The controller applies it by telling the car to keep its doors open for
another tick (a holdDoors counter that step counts down before closing). It never moves the
car, so it doesn’t affect the sweep or the dispatcher.
Interviewer asksHow would you scale this to a campus of fifty buildings?
Each building is independent, so each gets its own controller and loop, on its own hardware in practice. A campus dashboard reads each building’s published snapshot. Nothing about one building’s dispatching depends on another, so there’s no shared state to coordinate.
Further reading
Section titled “Further reading”- Strategy pattern, Command pattern, State pattern
- Thread safety basics, and Design a vending machine for the same single-owner loop
- Wikipedia: Elevator algorithm, SCAN and LOOK as disk scheduling algorithms
- Wikipedia: Destination dispatch, how lobby keypads change the dispatching problem
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.