Code a traffic light controller
Open in the interactive app: practice, quiz, mock interview →In one minute
- A signal controller is a timed state machine with a safety invariant: movements that can collide never show green together, and every green ends with yellow and all-red clearance before a conflicting green.
- Model phases (groups of compatible movements that go green together) and intervals (green, yellow, all-red). Validate the conflict table at construction so an illegal phase cannot exist at runtime.
- Demand sets green length: a minimum green, an extension each time a detector sees a car (the gap timer), and a maximum that starts when a conflicting road is waiting. Uncalled phases are skipped.
- Inject time and advance by computing the next due transition, not by sleeping. The controller becomes a deterministic function of its event log, testable and replayable after an incident.
- The shape is contention for a shared resource (the conflict zone) with fairness and a hard safety rule, the same shape as a database lock manager, a railway interlocking or a time-slicing CPU scheduler.
Requirements
Functional:
- One intersection with configurable movements (
N-through,S-left,E-ped) and a conflict table of pairs that may not move together. - Movements are grouped into phases served in cycle order, each with minimum green, maximum green, gap (extension), yellow and all-red times.
- Vehicle and pedestrian detectors place calls. A call on the green phase extends it; a call on any other phase asks to be served next time round. Phases with no call are skipped, and with no calls at all the controller rests in green on the current phase.
- Emergency preemption: a fire truck approaching on one phase ends the current green safely and holds that phase green until the preemption clears.
- Report every signal head's colour on demand, for the cabinet display and for logs.
Non-functional:
- Safety first: no conflicting greens, ever, and no green that skips its yellow and all-red. When unsure, go to flashing red (an all-way stop).
- Deterministic and replayable: given the configuration and a timestamped list of detector events, the output is the same every time.
- Timing resolution of 100 ms or better, on an embedded controller that runs for years without a reboot.
Out of scope for the interview: coordinating a corridor of signals for green waves, adaptive timing from a central system, transit priority and the cabinet's electrical design.
Entities and relationships
Use traffic engineering's own vocabulary; it makes the design easier to defend.
- Movement is a path through the junction: a vehicle through or left turn on one approach, or a pedestrian crossing. It is a value with a name. Each movement drives one signal head.
- ConflictTable is the set of movement pairs whose paths cross. It is data from the intersection's drawing, owned by a traffic engineer, not code.
- Phase is a named set of movements that are compatible with each other, plus its timing parameters. Phases are immutable once the controller starts.
- Interval is an enum: green, yellow, all-red. The controller is always in exactly one phase and one interval.
- Controller is the context object. It holds the current phase, the current interval and when it started, the pending calls, the time of the last extension on the green phase and any active preemption.
- Detector is anything that calls
detect(phase): an inductive loop in the road, a video sensor, a pedestrian push button. Detectors do not know about timing. - Clock supplies time. SignalHead and ConflictMonitor are observers of the controller's output.
The controller is composed of its phases and depends on the clock through an interface. Detectors depend on the controller, never the reverse, so a new sensor type needs no change in the core.
Class diagram
classDiagram
class Controller {
-phases: Phase[]
-current: int
-interval: Interval
-since: int
-calls: Map~int, int~
-lastExtension: int
-preempt: int
+detect(phase)
+preempt(phase)
+clearPreempt()
+state() PhaseInterval
+signals() Map~Movement, Color~
}
class Phase {
+name: string
+movements: Movement[]
+minGreen: int
+maxGreen: int
+gap: int
+yellow: int
+allRed: int
}
class Interval {
<<enumeration>>
GREEN
YELLOW
ALL_RED
}
class ConflictTable {
+conflicts(a, b) bool
}
class Clock {
<<interface>>
+now() int
}
class SignalHead {
<<interface>>
+show(color)
}
class ConflictMonitor {
+check(signals) bool
}
Controller "1" *-- "many" Phase
Controller --> Interval
Controller --> Clock
Controller ..> ConflictTable : validates at build
Controller --> SignalHead : notifies
ConflictMonitor ..> ConflictTable
LoopDetector --> Controller
PushButton --> ControllerKey flows
The flow below shows a side street car arriving while the main road is green. Note that the controller never sleeps: each call to it first catches up to the clock's current time, firing every transition that fell due in between.
sequenceDiagram
participant L as SideLoop
participant C as Controller
participant K as Clock
participant H as SignalHeads
participant M as ConflictMonitor
L->>C: detect(EW)
C->>K: now()
K-->>C: t=5
C->>C: catch up to t=5, record call EW at 5
Note over C: max timer for NS starts at 5
C->>K: now() on next tick
K-->>C: t=11
C->>C: NS gap expired, green to yellow at 11
C->>H: NS yellow
H->>M: field voltages
C->>C: yellow to all-red at 15
C->>H: all red
C->>C: all-red done at 17, EW green, clear EW call
C->>H: EW green
M->>M: no conflicting greens, OKDesign patterns used and why
- State, in its light form. Three intervals fit in an enum and one
firemethod. If preemption grows its own sequence (railway track clearance, dwell, exit), a State object per mode earns its place. - Strategy for phase selection. "Next called phase in cycle order" is the default; corridors want fixed sequences and adaptive systems pick by queue length. Behind
nextPhase(current, calls), the timing engine never changes. - Observer for outputs. Signal heads, the cabinet display and a log writer subscribe to interval changes. Publish after the transition commits, and never let a slow subscriber block the controller.
- Command for inputs. Detector events are timestamped immutable records. Logging them gives a replay harness: run yesterday's events against a new plan before deploying it.
- Validated factory for configuration. A phase plan arrives as data; the constructor refuses conflicting movements in one phase, a movement in two phases, or impossible timings. That is "make illegal states unrepresentable" applied to safety.
- Avoid a
TrafficLightclass per pole with its own timer. Independent timers drift, and the safety rule belongs to the whole junction, so it needs one owner.
Concurrency and edge cases
- One writer. Detector interrupts, preemption and the timer all change state. Funnel them through one queue or one mutex; no callback mutates the interval directly.
- Calls arriving during clearance. A car arriving on the green approach during yellow cannot extend it. Treat it as a call for that phase's next turn.
- Max timer start. Start the maximum green timer when a conflicting call arrives, not when the green began.
- Preemption mid-clearance. If the controller is already in yellow or all-red, let the clearance finish and then go to the preempted phase.
- Clock jumps. A wall clock set backwards by NTP or a technician must not rewind the controller. Use a monotonic clock for intervals and clamp time so it never goes back.
- Stuck detector. A loop stuck "on" extends green to max-out every cycle. Maximum green bounds the damage; diagnostics (no change in N minutes) switch the phase to a fixed recall and raise an alarm.
- Power and boot. After a power cut, start in flash, then run a startup all-red before the first green.
Down the stack
Layer 1The contract
At every instant, the set of movements showing green or yellow contains no conflicting pair. A green on phase P ends only after at least P's minimum green, and is followed by exactly P's yellow then P's all-red. Green ends when another phase is waiting and either no detector actuation arrived within the gap time or the maximum green has run since the first waiting call. With preemption active, the target phase is reached after at most one clearance and then held. Given the same configuration and the same timestamped event list, the sequence of intervals is identical.
Layer 2The mechanism
The engine stores the phase index, interval kind and start time, and computes one value: when the current interval is due to end. For yellow and all-red that is start plus duration. For green it is min(waitSince + maxGreen, max(start + minGreen, lastExtension + gap, waitSince)), undefined while nobody else waits, which is how resting in green falls out without a special case. Every entry point runs a catch-up loop that fires each due transition at its due time, not at now, so a late tick never stretches an interval. Real controllers do the same at larger scale: NEMA TS 2 defines dual-ring, eight-phase operation with barriers between the two street directions, and NTCIP 1202 defines the objects (minimum green, passage, maximum, yellow change, red clearance) a central system uses to read and write timings.
Layer 3The machine
A field controller is a small embedded computer in a roadside cabinet (newer ATC designs run Linux), ticking at 10 Hz because timings are set in tenths of a second. Its outputs drive solid-state load switches that put mains voltage on the lamps. Separately, a hardware conflict monitor (in NEMA cabinets, a Malfunction Management Unit) reads the actual field voltages on every channel and compares them with a programmed compatibility card. If it sees incompatible greens or a short yellow for more than a few hundred milliseconds, it trips a relay that puts the junction into flash, independently of the CPU. Detector loops are wire coils in the pavement; a vehicle's metal changes their inductance, and the detector card reports presence within tens of milliseconds. Yellow time comes from physics: the ITE kinematic formula adds about 1 second of perception-reaction time to approach speed divided by twice a comfortable deceleration, roughly 3 to 6 seconds for common speeds.
Why it's built this way
- A wrong green kills people, so safety cannot depend on the general-purpose software being correct.
- Hence two layers: software that validates the plan before it runs, and an independent hardware monitor that checks the actual outputs and fails to flash.
- Demand varies by minute, so fixed cycles waste green on empty roads; detectors and gap timers turn spare green into shorter waits.
- Actuation alone can starve a side street behind a busy main road, so a maximum green caps every extension once someone is waiting.
- Incidents get investigated and plans get changed by engineers, so the controller is a pure function of configuration plus event log, which can be replayed and audited.
Extensibility
- Pedestrian intervals. A pedestrian phase adds walk and flashing don't walk intervals before yellow. Its minimum green becomes "walk plus clearance", where clearance is crossing distance divided by a walking speed (the US MUTCD uses 3.5 feet per second). That is a change to timing data plus one interval kind, not a new controller.
- Coordination. Corridors run a common cycle length with offsets so platoons meet greens. Add a cycle timer and a "force-off" point per phase; the selection strategy gains a constraint, the engine stays.
- Adaptive control. A central system writes new plans through the validated factory; the controller swaps plans at a cycle boundary, never mid-green.
- Transit priority. A softer preemption: extend the current green a few seconds or start the bus's phase early. A new policy object, same engine.
- Many intersections. Each junction keeps its own controller and safety layer. Central management observes and configures but is never in the safety path; if the network drops, junctions keep running local plans.
Leadership lens
- Build first: the plan validator and the timing engine with a replay test harness.
- Ask in review: which component can make two greens appear, what independent check catches it, what happens when the clock jumps, and how a timing change is tested before it reaches the street.
- Separate ownership of configuration (traffic engineers, who own the conflict table and timings) from software (who own the engine). Give engineers a simulator that replays real detector logs.
- Safety-critical changes get their own process: replay a recorded day of traffic, stage the rollout, keep a fast revert.
- In an interview, spend the time on phases, the conflict invariant, actuation and preemption. A
Carclass with aspeedfield means you are modelling the wrong system.
Check yourself
Why model phases rather than individual lights with their own timers?
Show answer
The safety rule is about combinations of movements, so it needs one owner that changes them together. Independent timers drift and cannot enforce that two crossing paths are never green at once.
A side street car arrives after the main road has rested green for 10 minutes. With a 30 second maximum, when can the main green end at the latest?
Show answer
30 seconds after the side street call arrived, because the maximum timer starts on the conflicting call. Starting it at the beginning of green would end the main green instantly and ignore cars still arriving.
A preemption arrives during the yellow of the phase you want to leave. What should happen?
Show answer
Finish the yellow and the all-red, then go green on the preempted phase. Shortening clearance to save two seconds creates exactly the crash risk it exists to prevent.
Why fire a transition at its due time rather than at the moment the loop notices it?
Show answer
A late loop would stretch the interval and shift every later one. Firing at the due time keeps replays identical regardless of tick jitter.
Watch
- Phases and Stages, Traffic Signals Principles Part 1 (ITS Now): how movements are grouped into phases and stages, the vocabulary this design is built on.
- State Pattern, Design Patterns ep 17 (Christopher Okhravi): when a state machine deserves objects per state and when an enum is enough.
Free resources
- Traffic Signal Timing Manual (US FHWA, 2008): the reference for phases, actuation, clearance intervals and preemption.
- State pattern (Refactoring.Guru): the pattern behind interval transitions, and when an enum is enough.
- Traffic light control and coordination (Wikipedia): background on fixed-time, actuated and coordinated control.
- How Do Traffic Signals Work? (Practical Engineering): a readable tour of movements, loop detectors and actuated timing from a civil engineer.
- Interlocking (Wikipedia): the railway version of the same safety idea, where conflicting routes are locked out by design.