Traffic Control System
Signal phases, safe changes and emergency preemption
Design the controller for one signalized intersection: cycle the lights through safe phases, size each green from sensed queues, and give an emergency vehicle green without ever skipping a yellow or letting crossing traffic go together.
Overview
A traffic signal controller decides, second by second, which approaches of an intersection may go. Cars waiting on the north and south approaches can move at the same time because their paths do not cross; north and east cannot. The controller groups compatible approaches into phases, gives each phase a green, ends every green with a yellow and a short all-red pause, and moves on. Sensors report how many cars are waiting, and an ambulance may ask for green at any moment.
The problem tests three things. First, whether you make the safety rule (crossing movements are never green together) something the code checks, both when the phases are set up and after every change, instead of something the author promises. Second, whether you model the cycle as a small state machine with time passed in, so it can be tested without waiting. Third, whether you handle emergency preemption without breaking the safety rule: the green in progress may be cut short, but the yellow and all-red that follow it may not.
A strong answer draws the conflict table, the three-stage cycle and the preemption sequence, and says that the timing rule is a replaceable policy.
Scoping the problem
- One intersection, or a corridor of them? — A corridor adds offsets between neighbors (a green wave) — One; coordination is a follow-up
- Which movements: through only, or turns too? — Turn arrows add phases and conflicts — Through movements from four approaches
- Fixed timing or based on demand? — Decides whether sensors feed the timing — A policy object; demand-based in the demo
- What does an emergency vehicle get? — The cycle must be interrupted safely — Green on its approach as soon as it is safe, held until cleared
- Pedestrians? — Walk signals add intervals and push buttons — Follow-up question
- Who calls the controller, and from which threads? — Sensors, the timer and an operator console all write — Several threads; one lock
- How is time handled? — Real timers make tests slow and flaky — Time is passed in as now
The safety rule as data
Which movements cross is a fact about the intersection, so it is a table (CONFLICTS), not a set of if-statements. The constructor refuses any phase that contains a conflicting pair, and _check_safe re-checks the live lights after every change: any two approaches that are not red must not conflict. A bug elsewhere in the code then fails loudly instead of showing two greens.
- North — compatible — conflict — conflict
- South — compatible — conflict — conflict
- East — conflict — conflict — compatible
- West — conflict — conflict — compatible
Two phases cover the table: NS (north and south together) and EW. Real controllers also have an independent hardware conflict monitor in the cabinet that switches the intersection to flashing if it ever sees conflicting greens; the software check is the same idea one layer up.
The cycle
Each phase goes through three stages. Green lasts as long as the timing policy says; yellow and all-red have fixed lengths. The US Manual on Uniform Traffic Control Devices (2009 edition, Section 4D.26) requires a yellow after every green and recommends 3 to 6 seconds for it, and a red clearance (all-red) interval of no more than 6 seconds in most cases; the demo uses 4 and 2.
The controller never reads a clock. tick(now) compares now with ends_at and advances in a loop, stamping each change with the time it was due, not the time the tick arrived. That is what lets the demo drive an hour of signal time in a fraction of a second, and it is why the original design's threading.Timer chain was replaced: timers make tests wait in real time and leave threads to cancel.
Sizing the green
The timing rule is a strategy object with one method, green_time(phase, queues). The intersection calls it at the start of each green and knows nothing about the formula.
- FixedTiming(20) — always 20 s — 20 s — 20 s
- DemandTiming(2 s per car) (the demo) — min_green + 2 × longest queue, capped at max_green — 10 + 10 = 20 s — 10 + 40 = 50, capped at 40 s
The minimum keeps a phase from flashing past before a queue starts moving; the maximum stops one busy road from starving the other. A new rule (time of day, a corridor plan) is a new class.
Emergency preemption
When an ambulance approaches from the north, the controller must give north green as soon as it is safe and hold it. 'As soon as it is safe' has an exact meaning: the green in progress may end early, but its yellow and all-red run in full. The same manual (Section 4D.27) forbids shortening or omitting the yellow change and red clearance intervals when a signal goes into preemption.
- Green on the requested approach — holds it: ends_at becomes infinity
- Green on a conflicting phase — ends the green now; yellow and all-red follow at full length
- Yellow or all-red — lets it finish; the next green is the requested phase
- After clear_preempt — ends the held green through yellow and all-red, then continues with the phase after it
One lock
Sensors report queues from their own threads, a timer thread calls tick, and an operator or a detector calls preempt. All of them change stage, ends_at, lights or queues, so every public method takes the same lock, and the private helpers that run under it never take it again (Python's Lock is not reentrant). The demo starts four sensor threads that each report 300 queue lengths while the main thread ticks through an hour; _check_safe runs on every change and the phases must alternate.
What you will be able to do
- Write the conflict table and use it twice: to refuse unsafe phases at setup and to check the live lights after every change.
- Model the cycle as green, yellow, all-red, with time passed in, so tests run instantly.
- Size greens with a timing policy that has a minimum and a maximum.
- Handle emergency preemption without shortening a yellow or an all-red, and say what happens in each stage.
- Name the follow-ups: pedestrians, turn phases, a corridor green wave, and a manual override.
Functional requirements
- Four approaches (north, south, east, west), each with one signal that is red, yellow or green
- Phases group approaches that may be green together; a phase containing crossing movements is refused at setup
- Every green is followed by a fixed yellow and then a fixed all-red interval before the next phase
- The length of each green comes from a timing policy (fixed, or based on reported queue lengths) between the phase's minimum and maximum
- preempt(approach, now) gives that approach green as soon as it is safe and holds it; clear_preempt resumes the normal cycle
- report_queue(approach, vehicles) records sensor data for the next green
Non-functional requirements
- Safety: two conflicting approaches are never non-red at the same time; checked after every change
- Yellow and all-red are never shortened or skipped, including when entering and leaving preemption
- Thread safe: sensors, the timer and preemption requests call in from different threads
- Deterministic and testable: time is passed in; no timers or sleeps inside the controller
- New timing rules are new TimingPolicy classes; the intersection does not change
Interview questions
- How do you guarantee that crossing traffic never gets green at the same time?
- Why pass time in instead of using timers?
- How would you add pedestrian crossings with push buttons?
- How would you coordinate a row of intersections into a green wave?
- An operator wants to force a phase from a console. How does that fit?
Sign up free to read the full topic
More Low-Level Design topics
Start free on Cariara · Coding problems · System design studio