Engineering Atlas › Low-Level Design

Code a traffic light controller

12 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

Requirements

Functional:

Non-functional:

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.

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 --> Controller

Key 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, OK

Design patterns used and why

Concurrency and edge cases

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

  1. A wrong green kills people, so safety cannot depend on the general-purpose software being correct.
  2. 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.
  3. Demand varies by minute, so fixed cycles waste green on empty roads; detectors and gap timers turn spare green into shorter waits.
  4. Actuation alone can starve a side street behind a busy main road, so a maximum green caps every extension once someone is waiting.
  5. 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

Leadership lens

Check yourself

  1. 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.

  2. 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.

  3. 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.

  4. 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

Free resources

Open in the interactive app: practice, quiz, mock interview →