Appearance
9.7.15 — Traffic Signal Control
"Design a traffic signal controller for an intersection."
Most candidates hear "cycle through red, amber, green on a timer" and produce forty lines that do exactly that. It is the right shape and it is missing the thing that makes this problem worth asking: a traffic signal has a safety rule that must hold even when your software is broken. Two conflicting directions must never show green at the same time, and "my state machine never does that" is not good enough, because the state machine is code and code has bugs.
So this page is about two designs sitting on top of each other. The first is the normal one — phases, timers, sensors, pedestrians. The second is the one underneath it that assumes the first is wrong.
1. Get the vocabulary right first
The words matter here more than usual, because using "light" for three different things is what makes people's designs collapse.
| Term | What it means |
|---|---|
| Approach | One road arriving at the intersection |
| Movement | One legal path through it |
| Signal head | One set of lamps drivers look at |
| Phase | A set of movements green together |
| Cycle | One pass through all phases |
| Interval | One timed step inside a phase |
A movement is a direction of travel through the intersection, like "northbound, turning left" or "eastbound, straight ahead". This is the unit that matters, because conflicts are between movements, not between roads. Northbound-straight and southbound-straight are on the same road and never conflict. Northbound-left and southbound-straight are on the same road and conflict head-on.
A phase is a group of movements that are safe together and therefore go green together. On a normal crossroads there are usually four: north–south straight, north–south left turns, east–west straight, east–west left turns. Small intersections collapse this to two.
Getting to this vocabulary in the first two minutes is most of the battle, because the design writes itself once "phase" and "movement" are separate words.
2. The conflict matrix is the real model
The centre of this design is not the timer. It is a fixed table saying which movements may run together, and it comes from the geometry of the junction rather than from any code.
typescript
type MovementId = string; // "NB-through", "EB-left", "PED-N"
class Intersection {
readonly movements: readonly MovementId[];
readonly #conflicts: Map<MovementId, Set<MovementId>>; // (1)
conflict(a: MovementId, b: MovementId): boolean {
return this.#conflicts.get(a)?.has(b) ?? false;
}
isSafeTogether(group: readonly MovementId[]): boolean { // (2)
for (let i = 0; i < group.length; i++)
for (let j = i + 1; j < group.length; j++)
if (this.conflict(group[i], group[j])) return false;
return true;
}
}(1) The conflicts are data, loaded per junction. Every intersection in a city has a different shape, and the difference is entirely in this table. If conflicts were code, every junction would need a deployment.
(2) Any proposed phase is checked against the table. This is a whole-group check rather than a pairwise one at the call site, because a phase is only safe if every pair inside it is safe.
Where the check belongs is the design decision. Validate every configured phase when the controller starts, not on each transition. A junction with a badly configured phase should refuse to start rather than run for six weeks and then discover it during rush hour. This is the same reasoning as validating a snake-and-ladder board in its constructor (9.7.14): make an impossible configuration impossible to hold, rather than checking for it repeatedly.
3. The state machine, and the interval everyone forgets
typescript
type ControllerState =
| { kind: "green"; phase: PhaseId; since: Instant; extendedTo: Instant } // (1)
| { kind: "amber"; phase: PhaseId; until: Instant } // (2)
| { kind: "allRed"; from: PhaseId; next: PhaseId; until: Instant } // (3)
| { kind: "flashing"; reason: FaultReason }; // (4)(1) Green carries both when it started and how far it has currently been extended, because a sensor detecting an approaching car can push the end later — up to a maximum that no sensor can exceed.
(2) Amber carries only its end time, because nothing may change it. Its length comes from the speed limit and drivers' reaction time, typically three to six seconds, and it exists so that a driver too close to stop safely can clear the line legally. Shortening it because the junction looks empty is how you cause a collision.
(3) All-red carries both the phase ending and the phase starting. That is not decoration: the clearance time depends on the width vehicles must cross, which depends on which movements were running and which are about to.
(4) The failure state, which section 7 is about. It is part of the union rather than a boolean beside it, so "flashing while also green" cannot be represented (9.4.14).
The all-red interval is the answer to the most common follow-up. A car that entered the junction legally on amber is still inside it when amber ends. If the crossing direction goes green at that instant, they meet in the middle. So every phase change includes a short period where everything is red, long enough for a vehicle to clear the widest conflicting path. The arithmetic is worth being able to state: a 20-metre crossing at 50 km/h, which is about 14 metres per second, takes roughly 1.5 seconds, so a 2-second all-red is a reasonable number to say out loud. Giving a number turns a memorised phrase into a demonstration that you know why the interval exists.
Timing constraints that are not optional:
Minimum green stops a phase being cut short the instant it starts. Without it, a sensor pattern can make a light flicker green and away again, which is dangerous and infuriating.
Maximum green stops a busy direction holding the junction forever. It is the starvation guarantee, and it is the same idea as aging a low-priority task so it eventually runs (9.5.3).
Amber is fixed. It is calculated from the approach speed, and it never varies with traffic.
4. Which phase next: the policy seam
The transition mechanics are fixed by safety. Which phase runs next, and for how long, is where all the variation lives, so it is an interface.
typescript
interface SignalPolicy {
nextPhase(current: PhaseId, demand: DemandSnapshot): PhaseId; // (1)
greenDuration(phase: PhaseId, demand: DemandSnapshot): Seconds; // (2)
}(1) Given where we are and what is waiting, what runs next.
(2) How long to hold it, always clamped by the controller to the configured minimum and maximum. The policy suggests; the controller enforces the safety bounds. That division matters — a policy with a bug should be able to make traffic worse, never make it unsafe.
Three policies worth naming, in increasing order of how much they know:
Fixed time. Phases run in a fixed order for fixed durations, usually with a different plan for morning, evening and night. It needs no sensors, it is perfectly predictable, and it is still the most common controller in the world. It wastes green on empty approaches.
Actuated. Sensors in the road detect waiting and approaching vehicles. A phase with no demand is skipped; a phase with continuing arrivals is extended up to its maximum. This is a large improvement for one junction and it is where most interview answers should land.
Adaptive. The controller optimises across a whole corridor using traffic predictions. It genuinely helps and it is a research-grade problem, so name it and move on rather than pretending to design it in the remaining ten minutes.
Coordination between junctions is worth mentioning because it is what drivers actually experience. A "green wave" gives consecutive junctions along a road a fixed offset, meaning junction B turns green a set number of seconds after junction A, timed so a car travelling at the speed limit meets green after green. This needs the junctions to share a common cycle length and a synchronised clock, which makes it a small distributed-systems problem hiding in a street corner — and clock drift between controllers degrades the wave exactly the way 10.3 describes.
5. Pedestrians, and the button that is a request
Pedestrian movements sit in the conflict matrix like any other movement, which is why the vocabulary in section 1 was worth setting up. A pedestrian crossing north conflicts with vehicles turning across it, and the table says so.
The pedestrian signal has its own three intervals, and the middle one is the interesting one:
Walk — start crossing.
Flashing don't walk — do not start; finish if you have. Its length is computed from the crossing width and an assumed walking speed, and that assumption is a policy number rather than a constant. Crossings near a hospital or a school use a slower speed, which is a good example of a domain rule that looks arbitrary until you know why.
Don't walk — the crossing is closed.
The button is a latched request, not a command. Pressing it does not change the lights; it records that a pedestrian phase is wanted, and the flag stays set until that phase is served. Two consequences follow, and both are worth saying because they are the design content of this section.
First, pressing it repeatedly does nothing, because setting a boolean that is already true is not a change. This is why the folklore about pressing the button faster is wrong, and mentioning it is a cheap way to show you understand what latching means.
Second, a latched request must eventually be served, which is the same starvation guarantee as maximum green. A design where a pedestrian request can be indefinitely postponed by continuous vehicle demand is a design that strands people, and the fix is that the request enters the phase selection as demand rather than as a hint.
6. Emergency preemption: the interrupt that still obeys physics
An ambulance approaching gets priority, and this is where a good answer separates from a fast one.
typescript
function preempt(state: ControllerState, target: PhaseId): ControllerState {
switch (state.kind) {
case "green":
return { kind: "amber", phase: state.phase, until: now().plus(AMBER) }; // (1)
case "amber":
case "allRed":
return state; // (2)
case "flashing":
return state; // (3)
}
}(1) Preemption during green does not jump to the emergency phase. It ends the current green early — which is allowed, subject to minimum green — and then the normal amber and all-red intervals run in full. The emergency vehicle is served at the next phase selection.
(2) During amber or all-red, preemption does nothing at all. Those intervals exist for physics and cannot be shortened by any priority, however urgent. A candidate who cuts amber short for an ambulance has just designed a system that puts an ambulance into a crossing car.
(3) In the failure state, nothing overrides. If the controller does not trust itself, it must not act on a remote request.
The general lesson transfers well beyond traffic: high priority may reorder what happens next, but it may never skip a step that exists for safety. The equivalent in ordinary software is a "force" flag that bypasses validation because someone senior asked — the priority was real and the shortcut was still wrong.
Preemption also needs a return path. After the emergency vehicle passes, the controller must resume the normal cycle without leaving one approach starved for an entire extra cycle, and if the junction was coordinated with its neighbours, the offset has to be recovered gradually rather than by one large correction. Naming the recovery, not just the interrupt, is what makes this answer complete.
7. The design underneath: assume the software is wrong
Everything above is one program, and one program can crash, hang, or have a bug that produces a phase the conflict matrix forbids. Real signals are built on the assumption that this will happen.
There is a separate device called a conflict monitor, and it is not part of the controller. It watches the actual voltage on the lamp circuits. If it ever sees two conflicting movements powered green at the same time — or if the controller stops sending a periodic "I am alive" signal, or if a green lamp has failed and the junction is showing nothing at all — it physically disconnects the controller and drops the junction into flashing mode: flashing red on the minor road, flashing amber or flashing red on the major road, meaning drivers treat it as a stop junction and use human judgement.
Three properties of this arrangement are worth stating explicitly, because they are the transferable part:
It is independent. A bug in the controller's code cannot affect it, because it does not run that code. Any safety check living inside the thing it is checking can be defeated by the same bug that broke the thing.
It fails to a safe state, not to a stopped one. Turning everything off is not safe — an unlit junction at night is worse than a flashing one. The safe state is a degraded but usable one, which is nearly always the right target when a system must fail in the physical world.
It needs a heartbeat, not just an error signal. The controller proves it is alive on a schedule. A design that relies on the controller reporting its own failure cannot detect the case where the controller is hung, which is the most common way software stops working (10.10).
The software equivalent of a conflict monitor is a watchdog process outside the application that restarts or isolates it, and the reason it is worth building is the same: the component best placed to detect a failure is never the failing component.
8. One event loop per junction
The controller receives sensor events, pedestrian button presses, preemption requests, clock ticks and remote configuration changes. They arrive at unpredictable times and every one of them can change the state.
The answer is the same as the elevator's (9.7.28) and the online board game's: one mailbox per junction, events processed one at a time, with a single transition function as the only writer of state (9.5.4).
typescript
class SignalController {
#state: ControllerState;
handle(event: SignalEvent): ControllerState {
this.#state = this.#transition(this.#state, event); // (1) one writer
this.#apply(this.#state); // (2) then drive the lamps
return this.#state;
}
}(1) Every event goes through one function, so the safety argument is a matter of reading a single switch statement rather than of auditing every place that touches a lamp.
(2) Applying the state to the hardware is separate from computing it. That separation means the same controller drives a real junction, a simulator, or a screen, and it is the reason a city can replay a day of recorded sensor data through the exact controller that will run on the street.
Clock handling is the detail that bites. A phase does not end because a timer fired; it ends because the current time has passed the recorded end time. Storing an end instant and comparing against the clock, rather than counting down, means a delayed or coalesced tick cannot lengthen a phase. The same rule appears in every timed design in this chapter, including hold expiry in 9.7.9.
9. What the interviewer will push on
"What stops two directions being green at once?" Two answers, in this order. In software, phases are validated against a conflict matrix at startup, so an unsafe phase cannot even be configured. In hardware, an independent conflict monitor watches the lamp circuits and drops the junction to flashing if it ever sees a conflict or loses the controller's heartbeat. A candidate who gives only the first answer has not understood the problem: the check that lives inside the thing it checks dies with it.
"Walk me through changing from one phase to the next." Green ends — either at maximum, or early because demand ran out, but never before minimum. Then a fixed amber, whose length comes from the approach speed. Then an all-red clearance, long enough for a vehicle that entered on amber to leave the junction. Only then does the next green start. The missing all-red is the single most common gap, and giving the arithmetic — a 20-metre crossing at about 14 metres per second is roughly 1.5 seconds, so 2 seconds — is what separates knowing the interval exists from knowing why.
"An ambulance is coming. Cut the lights to green." No. Preemption may end a green early and may change which phase is chosen next, but amber and all-red run in full, because they exist for physics rather than for policy. Then volunteer the return path: resuming the normal cycle without starving the approach that was interrupted, and re-synchronising with neighbouring junctions gradually if the corridor is coordinated.
"Someone presses the pedestrian button ten times." It is a latched request, so the second press changes nothing that the first did not already change. The design point behind it is that a latched request must be guaranteed to be served — it enters phase selection as demand, not as a preference — or continuous vehicle traffic can strand a pedestrian indefinitely.
"How do you stop one busy road holding the junction forever?" Maximum green. It is the same starvation guarantee as aging a low-priority task, and it is why the policy object only suggests a duration while the controller clamps it.
"Make the whole corridor work together." Give consecutive junctions a shared cycle length and a fixed offset so a car at the speed limit meets green after green. Then name the cost honestly: it needs synchronised clocks, it optimises one direction at the expense of the other, and it degrades as the controllers' clocks drift apart.
The thing to volunteer that nobody asks for: the flashing mode is not an error state, it is a product state. Someone had to decide which road flashes red and which flashes amber, that maintenance crews are alerted automatically, and that the junction stays usable by human judgement while broken. Candidates treat failure as an exception to be logged. Naming the degraded mode as something designed, with its own behaviour and its own configuration, is what makes this answer sound like it came from someone who has built a real machine.
Recall
- Separate movement (one path through the junction) from phase (a set of movements green together). Conflicts are between movements.
- The core model is a conflict matrix loaded as data per junction, and every configured phase is validated at startup, not per transition.
- A phase change is four intervals: green (min → max, extendable by sensors), fixed amber, all-red clearance, then the next green. The all-red is the piece most designs are missing; a 20-metre crossing at ~14 m/s needs roughly 2 seconds.
- Minimum green stops flicker. Maximum green is the starvation guarantee. Amber never varies with traffic.
- Which phase runs next is a policy interface — fixed-time, actuated by sensors, or adaptive. The policy suggests a duration; the controller clamps it to the safety bounds.
- The pedestrian button is a latched request, so pressing it repeatedly does nothing, and a latched request must be guaranteed to be served.
- Preemption may end a green early; it may never shorten amber or all-red. Priority reorders what happens next, it does not skip a step that exists for safety.
- A separate conflict monitor watches the lamp circuits and the controller's heartbeat, and drops the junction to flashing — a degraded but usable state, not an off state. The failing component can never be its own safety check.
- One mailbox per junction, one transition function as the only writer, and phases end by comparing the clock against a stored end instant rather than by counting down.
Self-test: What is the difference between a movement and a phase? Which interval do candidates leave out, and how long should it be? What may preemption not do? Why does pressing the button twice change nothing? What does the conflict monitor watch, and why can it not live inside the controller?
Quiz Bank
FoundationalDesign the state machine for a four-phase intersection, and say what each interval is for.
Set the vocabulary first, because the design depends on it. A movement is one legal path through the junction, like northbound-through or eastbound-left. A phase is a set of movements that are safe together and therefore go green together. A typical crossroads has four: north–south through, north–south left turns, east–west through, east–west left turns.
The states are a union, not a set of booleans:
typescript
type ControllerState =
| { kind: "green"; phase: PhaseId; since: Instant; extendedTo: Instant }
| { kind: "amber"; phase: PhaseId; until: Instant }
| { kind: "allRed"; from: PhaseId; next: PhaseId; until: Instant }
| { kind: "flashing"; reason: FaultReason };Green carries how far it has been extended, because sensors can push its end later. Amber carries only an end time, because nothing may change it. All-red carries both the outgoing and the incoming phase, because the clearance time depends on the geometry of both.
Now each interval and its job.
Green, between a minimum and a maximum. The minimum stops the light flickering on and off when sensor readings fluctuate. The maximum stops a busy approach holding the junction forever, which is the starvation guarantee.
Amber, a fixed length calculated from the approach speed and reaction time, usually three to six seconds. It exists so a driver too close to stop safely can clear the stop line legally. It does not vary with traffic, ever.
All-red, where every movement is red. A vehicle that entered legally on amber is still inside the junction when amber ends, and if the crossing direction went green at that moment they would meet. Length comes from the widest conflicting path: about 20 metres at roughly 14 metres per second is 1.5 seconds, so 2 seconds is a defensible number.
Flashing, the fault state, and part of the union rather than a flag beside it. Making it a member of the union means "flashing while green" cannot be represented at all.
Two structural points worth adding.
The safe combinations come from a conflict matrix held as data per junction, and every configured phase is checked against it at startup. A junction with an unsafe phase should refuse to start rather than discover the problem in service.
Phases end by comparing the current time against a stored end instant, not by counting a timer down. A delayed or coalesced tick then cannot silently lengthen a phase, which is the same rule that governs hold expiry in the booking problems.
AppliedTraffic sensors are added. Design an actuated controller that skips empty phases and extends busy ones, without letting any approach starve.
Separate what is safe from what is smart, because that split is the answer. The intervals and their ordering are fixed by physics and are the controller's business. Which phase runs next and how long its green lasts are traffic decisions and belong behind an interface.
typescript
interface SignalPolicy {
nextPhase(current: PhaseId, demand: DemandSnapshot): PhaseId;
greenDuration(phase: PhaseId, demand: DemandSnapshot): Seconds;
}The controller clamps whatever the policy returns to the configured minimum and maximum green for that phase. That is a deliberate division of authority: a bug in the policy can make traffic worse, and it must not be able to make the junction unsafe or make an approach wait forever.
Skipping an empty phase is the easy half. If no detector on a phase's approaches reports waiting traffic and no pedestrian request is latched for it, the policy simply does not select it. The saving is real — on a quiet night, a junction that never turns green for an empty side road moves everybody through faster.
Extending a busy phase is the half with the trap. The natural rule is to push the green's end later each time a detector sees another vehicle approaching, so a continuous stream keeps the phase alive. Left alone, that rule starves the cross street during rush hour, which is exactly why maximum green exists as a hard bound in the controller rather than as a suggestion in the policy.
Pedestrian requests enter as demand, not as a hint. A latched button press is an input to nextPhase on the same footing as a vehicle detector. If it were merely a preference, continuous vehicle traffic could postpone it indefinitely, and a pedestrian would be standing at a crossing that is technically working correctly.
Detector failures need a defined behaviour, and the safe direction is counter-intuitive. A detector that reports nothing is indistinguishable from an empty road, so a broken detector means an approach that is never served. The rule is therefore: a detector that has reported no vehicle for an implausibly long period is treated as failed, and a failed detector makes its phase run on fixed timing rather than being skipped. Failing towards "serve it anyway" costs a little delay; failing towards "skip it" strands a whole road.
Two extras to volunteer.
Actuation changes the cycle length continuously, which quietly breaks coordination with neighbouring junctions. A corridor running a green wave needs a common cycle length, so junctions in a coordinated group are usually only allowed to actuate within the slack of a fixed cycle. That tension between local optimisation and corridor optimisation is real and worth naming.
The measurement that tells you whether it worked is not average delay, which hides the failure. It is the distribution — particularly the worst waits on the minor approaches, because those are the people a well-meaning actuated controller quietly punishes.
InterviewHow do you guarantee two conflicting directions are never green at the same time, given that your controller software will eventually have a bug?
Answer in two layers, and say that it is two layers, because the second one is the point of the question.
Layer one, in software: make the unsafe state unrepresentable and validate the configuration up front. The state is a union where exactly one phase can be green, so "two phases green" is not a state the type system permits. The junction's conflicts are a matrix loaded as data, and every configured phase is checked against that matrix when the controller starts. A junction whose configuration contains an unsafe phase refuses to run at all.
That is a good design and it is not sufficient, for one reason worth stating plainly: it is enforced by the same program that might be wrong. A memory bug, a stuck loop, or a relay that welds shut are all failures the controller cannot detect about itself.
Layer two, in hardware: an independent conflict monitor. It is a separate device watching the actual voltage on the lamp circuits, and it trips on three things — two conflicting movements powered green together, the absence of the controller's periodic heartbeat, and a lamp that has failed such that a direction is showing nothing. When it trips, it physically disconnects the controller and puts the junction into flashing mode.
Three properties of that arrangement are the transferable lesson.
Independence. It does not run the controller's code, so no bug in that code reaches it. Any check living inside the component it checks can be defeated by the same fault that broke the component.
It watches a heartbeat, not an error report. A design that waits for the controller to announce its own failure cannot detect a hung controller, which is the most common way software stops working. The proof of life has to be periodic and positive.
It fails into a degraded but usable state. Turning the junction off is not safe — an unlit crossroads at night is worse than a flashing one. Flashing red on the minor road and flashing red or amber on the major road turns the junction into a stop junction that humans can operate.
The equivalent in ordinary software is a watchdog outside the process that restarts or isolates it, and the reasoning is identical: the component best placed to notice a failure is never the failing component.
The thing to add unprompted is that flashing mode is designed, not accidental. Which road flashes which colour is a configured decision, maintenance is alerted automatically, and the junction stays usable meanwhile. Treating the failure state as a product feature rather than as an exception is what makes the answer sound like it came from someone who has built a machine that runs unattended.
StaffScale this to a city: thousands of junctions, a central control room, remote configuration changes, and a green wave along the main corridors. What are the real problems?
Say the boundary first, because it determines everything else: safety is local and optimisation is central. Each junction must remain safe with no network at all, so nothing central may ever be in the path of a decision that keeps people alive. The centre may suggest plans, collect data and change configuration; it may never be required for the junction to run. A design that phones home before changing phase has invented a way for a network outage to stop a city.
So each junction is autonomous, holding its own conflict matrix, its own phase configuration and its own policy. Losing the centre degrades optimisation, not safety — which is the same argument as putting the conflict monitor in hardware, one level up.
Then the four problems that are genuinely hard.
Configuration deployment is the dangerous operation. A bad plan pushed to a thousand junctions at once is a city-wide incident. Every plan is validated against that junction's conflict matrix on the junction before being accepted, rollout is staged, and a junction that receives a plan it cannot validate keeps running the previous one and reports the rejection. Also: the change must take effect at a phase boundary, never mid-cycle, because reconfiguring a running phase is precisely the way to produce a transition nobody analysed.
Clock synchronisation is the corridor's foundation. A green wave is junctions agreeing on a cycle length and holding fixed offsets from a common time reference. Drift of a couple of seconds is enough to turn a wave into a line of brake lights, so junctions need a real time source and the design has to handle the case where one of them loses it — and the answer is that it falls back to running its own cycle without coordination rather than trying to keep an offset from a clock it no longer trusts (10.3).
Coordination is directional, and someone has to choose. A wave optimised for the morning inbound flow makes the outbound direction worse. That is not a bug to be engineered away; it is a trade with a right answer that changes by time of day, and the design should make the plan a schedulable thing rather than a constant.
Observability is the part that decides whether any of this works. The centre needs per-junction health — heartbeats, monitor trips, detector failures — and per-junction traffic outcomes, and it needs to notice a junction that has been in flashing mode for six hours because nobody watched the alert. Detector failure is the silent one: it looks exactly like an empty road, so the detection rule has to be "implausibly quiet for too long" rather than an error report.
Two design decisions that follow from the scale rather than from the traffic.
Data flows up, control flows down, and they are different systems. Telemetry is high volume, loss-tolerant and eventually consistent. Configuration is low volume, must not be lost, and must be exactly ordered per junction. Building both on one pipe forces the wrong guarantees on one of them.
Rollback must be as easy as rollout. Every junction keeps the previous working plan and can be told to revert with one instruction, because at three in the afternoon on a Friday the question is never "what went wrong" but "how fast can we undo it".
What I would monitor above all, since it is the number that quietly rots: the count of junctions running in a degraded mode of any kind — flashing, uncoordinated, or on fixed timing because a detector failed. Each one individually is a minor fault, and a slowly rising total is a city becoming steadily worse at moving traffic while every single alert looks unimportant.
Flashcards
FlashMovement versus phase
A movement is one legal path through the junction. A phase is a set of movements that are safe together. Conflicts are between movements, and they live in a matrix loaded as data per junction.
FlashThe four intervals
Green (min → max, extendable), fixed amber, all-red clearance, next green. All-red is the one people forget: a 20-metre crossing at ~14 m/s needs roughly 2 seconds.
FlashWhat preemption may not do
Shorten amber or all-red. Priority may end a green early and choose the next phase; it may never skip an interval that exists for physics.
FlashThe pedestrian button
A latched request. Pressing it again sets a boolean that is already true. The design rule is that a latched request must be guaranteed to be served, or continuous traffic strands the pedestrian.
FlashThe conflict monitor
Separate hardware watching lamp voltages and the controller's heartbeat. Trips to flashing mode. The lesson: a safety check inside the component it checks dies with that component.
FlashDetector failure direction
A dead detector looks exactly like an empty road. Treat implausibly long silence as failure and fall back to fixed timing for that phase — failing towards "serve it anyway" costs delay, failing the other way strands a road.
Next: 9.7.16 — the coffee machine, where two drinks compete for the same milk and the inventory stops being a shelf count.