Traffic Signal
State pattern drives clean phase transitions across an intersection. Each signal knows only its own color. A controller coordinates them so conflicting directions never get green simultaneously.
Key Abstractions
Manages an intersection, coordinates signal phases so conflicting directions never overlap
One direction's light. Holds current state and transitions based on timing config.
Enum: RED, YELLOW, GREEN. Each state knows its duration and valid next state.
Strategy for per-phase durations — FixedTimingStrategy and RushHourTimingStrategy implementations
Groups multiple signals into a coordinated unit with named directions
Observer that preempts normal cycling when an emergency vehicle approaches
Class Diagram
Why This Design
Traffic signals look simple on the surface. Red, yellow, green, repeat. But the interesting constraint is coordination: two perpendicular directions can never both be green at the same time. That makes it a state machine problem, and state machines done well push all the transition logic into the states themselves.
A signal doesn't decide when to change. It's a dumb light that displays whatever color it's told. The controller owns the timing. It knows which phase is active, when to transition to yellow, and when to flip to the next phase. This separation is important because the moment you give signals autonomy, you've created a race condition. Two signals independently deciding they're green is a simulated pileup.
Requirements
Functional
- Support a four-way intersection with independent signals per direction
- Cycle through phases: North/South green while East/West red, then swap
- Yellow transition between every green-to-red change
- Emergency override: force one direction green, all others red
- Configurable timing per phase
Non-Functional
- No two conflicting directions can be green simultaneously (safety invariant)
- Timing strategy must be swappable at runtime (rush hour, nighttime, adaptive)
- Thread-safe signal state changes
- Observer notifications for pedestrian crossings and emergency systems
Design Decisions
Why State pattern over if/else chains?
Consider what happens without it: every tick has a nested if current == GREEN and elapsed > duration then set YELLOW elif current == YELLOW and elapsed > yellow_duration then set RED... and that's just for one signal. Multiply by four directions and add phase coordination. It becomes a wall of conditions. With the State pattern, each phase encapsulates its own green directions and durations. Transitions are data, not code branches.
Why signals are passive (controller-driven)?
This is a safety design. If signals drove their own transitions, you'd need distributed coordination to prevent conflicts. A single controller is the simplest correct solution. It guarantees the invariant: at most one set of non-conflicting directions is green at any time. Centralized control is appropriate here because a real intersection has a single controller box.
Why Observer for emergency vehicles?
Emergency override could be a method call on the controller. But that couples emergency detection to traffic control. In practice, emergency preemption systems are separate hardware. Observer keeps them decoupled: sensors detect the vehicle, fire an event, the controller reacts. Adding a train crossing sensor later doesn't touch the controller code.
Why Strategy for timing instead of config values?
Config values work for fixed timing. But adaptive timing needs sensor data and real-time computation. Making timing a strategy lets you have a FixedTimingStrategy that returns constants and an AdaptiveTimingStrategy that reads queue lengths from sensors and adjusts on the fly. Same controller, fundamentally different timing behavior.
Interview Follow-ups
- "How would you handle a left-turn signal?" Add more phases. A left-turn phase gives the turning direction a protected green while opposing traffic stays red. The phase list grows but the controller logic stays identical.
- "What about pedestrian crossing buttons?" Register a PedestrianObserver. When a button press arrives, extend the all-red clearance interval before the next green phase to give pedestrians time.
- "How would you make this adaptive?" Implement AdaptiveTimingStrategy. It reads from traffic sensors (loop detectors, cameras) and adjusts green duration proportional to queue length. Heavy traffic on one approach gets more green time automatically.
- "What happens if the controller crashes?" Fail to flashing red (all directions). This is the safe default that real traffic signals use during power failures. Each direction treats it as a stop sign.
45-Minute LLD Playbook
Each phase is what the interviewer expects you to do and say. Concrete steps, not topic hints. Diagrams are what you would sketch on the board.
- 15 min
Clarify intersection shape, safety invariants, and overrides
GoalPin the four-way layout, the no-conflicting-greens invariant, yellow handling, and emergency preemption. End with the diagram-vs-code question.
Do & Say- ASK·1Open with: Four-way intersection with one signal per approach, right? Are left-turn phases in scope, or just through traffic? Lock four directions (NORTH, SOUTH, EAST, WEST), through traffic only, two phases (NS green then EW green). Park left-turn protected phases, pedestrian crossings, and rail crossings as v2.
- SAY·2Pin the safety invariant: At most one set of non-conflicting directions is green at any time. NS and SOUTH can both be green together because they do not conflict, but NS and EAST cannot. The controller owns that invariant, signals are passive and never self-transition.
- SAY·3State the yellow rule: Every green-to-red transition goes through yellow first, configurable duration (default 3 ticks). Jumping straight from green to red is unrealistic and breaks any driver model. Yellow is built into Phase.total_duration, not bolted on later.
- SAY·4Confirm the emergency override: When an emergency vehicle approaches, the controller flips all signals to RED. Then sets the requested direction to GREEN, and holds until release. After release, we resume from the current phase index, not restart the cycle.
- ASK·5Ask the process question: Class diagram on the board first, or jump into code?. I would draw a quick state diagram showing the two phases, since the phase transition is what we are really designing. And then code from there.
Interviewer is grading: You insist on the no-conflicting-greens invariant before drawing anything. You park left-turn phases and pedestrian buttons explicitly. You name yellow as part of every green-to-red transition. You ask the diagram-vs-code question.
- 25-10 min
Sketch the phase sequence and the controller-driven signals
GoalLock the Phase data structure, the tick loop, the controller-is-active versus signals-are-passive split, and where Strategy plugs in.
Do & Say- SAY·1Name the cast: TrafficController, Intersection (group of 4 signals), Signal (one direction, holds a SignalState), SignalState enum, Phase (which directions are green plus durations), TimingStrategy interface with FixedTimingStrategy and RushHourTimingStrategy, SignalObserver interface.
- WRITE·2Write Phase signature on the board: Phase(green_directions: Set[str], green_duration: int, yellow_duration: int). total_duration = green + yellow. Say: A phase is a slice of time where a specific set of directions is green. The strategy returns a list of Phases. The controller cycles through them.
- WRITE·3Write the controller tick signature: tick() advances tick_in_phase. When tick_in_phase == green_duration, intersection.set_yellow(current_phase.green_directions). When tick_in_phase >= total_duration, advance current_phase_idx and intersection.set_phase(next.green_directions). Say: One tick is one time unit. The controller is the only thing that decides which signal is green at any moment. Signals are dumb lights.
- SAY·4Name the controller-is-active versus signals-are-passive split out loud: If signals decided their own timing, two perpendicular directions could both decide they are green at the same tick, and you have a simulated collision. Centralized control is the simplest way to enforce the invariant.
- SAY·5If a diagram was requested, draw the two-phase cycle: Phase 0 (NS green) -> NS yellow -> Phase 1 (EW green) -> EW yellow -> back to Phase 0. Annotate the durations. On the side, show Intersection holding four Signals, and TrafficController holding the phase list and current_phase_idx.
Interviewer is grading: Phase is a data class with explicit green_duration and yellow_duration. Tick logic separates the green-to-yellow boundary from the yellow-to-next-phase boundary. You call out 'signals are passive' as the safety invariant rather than just a style choice.
- 325 min
Code in this sequence (bottom-up)
GoalBuild the enum, Signal, Phase, the two TimingStrategy implementations, the Observer interface, Intersection, then the TrafficController. Matches pythonCode order.
Do & Say- SAY·1Start with SignalState enum (RED, YELLOW, GREEN). Signal class with direction string and state field, default RED. Lock so reads and writes are atomic, since observers may run on other threads. Say: Signal is the dumb light. It exposes state and never decides when to change. (~3 min)
- SAY·2Code Phase. Constructor takes green_directions (set), green_duration, yellow_duration with a default of 2. Compute total_duration = green + yellow on construction. __repr__ for debugging. Say: Phase is plain data. The controller reads it. (~2 min)
- SAY·3Code TimingStrategy abstract with get_phases() -> list[Phase]. Then FixedTimingStrategy returning [Phase(NS, 30, 3), Phase(EW, 25, 3)] and RushHourTimingStrategy returning [Phase(NS, 45, 3), Phase(EW, 15, 3)]. Say: Strategy here is timing config as code. AdaptiveTimingStrategy that reads sensor data slots in the same way. (~4 min)
- SAY·4Code SignalObserver abstract with on_signal_change(direction, old_state, new_state). Then PedestrianCrossingObserver that prints walk on/off based on transitions, and EmergencyObserver that prints clearance messages on GREEN. Say: Observers are for downstream systems that care about transitions. Pedestrian crossing controllers, emergency vehicle hardware, replay logs. (~3 min)
- SAY·5Code Intersection. Constructor builds four signals (NORTH, SOUTH, EAST, WEST), all RED. set_phase(green_directions) sets GREEN for matches, RED otherwise, notifies on change. set_yellow flips to YELLOW. set_all_red flips all RED. Say: Intersection mediates between controller and signals, batches phase transitions atomically. (~6 min)
- SAY·6Code TrafficController. Constructor takes timing_strategy, builds phases, sets idx=0, set_phase for first. tick handles two boundaries: at green_duration set_yellow on current greens, at total_duration advance idx and set_phase for next. trigger_emergency flips all red then greens the requested direction. Say: Two-boundary tick gives us yellow in between. (~6 min)
- SAY·7Walk one cycle as self-check. NS green 30 ticks. Tick 30: set_yellow(NS). NS yellow 3 ticks. Tick 33: advance to phase 1, set_phase(EW). EW green 25 ticks. Tick 58: set_yellow(EW). Tick 61: back to phase 0. Invariant: NS and EW never green simultaneously, every green-to-red goes through yellow. (~1 min)
Interviewer is grading: Signal exposes state via a property and never self-transitions. Intersection batches the phase change so observers fire once per direction-changed. tick has explicit two boundary checks, not a single 'is the phase over' check. emergency override flips all red before setting the requested direction green.
- 45 min
Trade-offs, extensions, and wrap-up
GoalDefend centralized control, defend Strategy for timing, volunteer one extension, close with one summary sentence.
Do & Say- SAY·1Trade-off one, centralized controller over distributed signals: Distributed signals would need consensus to prevent two perpendicular greens at the same tick. That is overkill for a single intersection. One controller box owns the invariant and the failure mode (controller crash) is well understood (flash to red, treat as four-way stop).
- SAY·2Trade-off two, Strategy over hardcoded durations: Constants work for a fixed-timing intersection. But rush hour, nighttime, and queue-length-adaptive timing all need different durations. Strategy makes adding AdaptiveTimingStrategy (reads loop detectors) a new class, not an edit to TrafficController.
- SAY·3Extension to volunteer, left-turn phases: Add more Phase entries. Protected NS-left phase gives left-turning NS traffic a green arrow while opposing SS stays red. The Phase list grows, the controller logic does not change. That is the value of treating phases as data.
- WATCH·4Be ready for the controller crash question: Fail to flashing red on all directions. The safe default real signals use during power failures or faults. Each direction treats it as a stop sign. A watchdog timer detects the crash and triggers flash mode.
- SAY·5Close with one sentence: State enum on each signal. Controller drives all transitions. Strategy swaps timing for rush hour or adaptive. Observer for crossings and preemption. The invariant is at most one set of non-conflicting greens at any time, enforced by centralized control.
Interviewer is grading: You defend centralized control with a clear failure mode (flash to red). You frame Strategy as 'AdaptiveTimingStrategy reads sensors, no controller change.' You volunteer left-turn phases unprompted. You name flash-to-red as the safe failure mode.
Code Implementation
from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum
from threading import Lock
class SignalState(Enum):
RED = "RED"
YELLOW = "YELLOW"
GREEN = "GREEN"
class Signal:
"""One direction's traffic light. Passive: the controller drives transitions."""
def __init__(self, direction: str):
self.direction = direction
self._state = SignalState.RED
self._lock = Lock()
@property
def state(self) -> SignalState:
with self._lock:
return self._state
@state.setter
def state(self, new_state: SignalState) -> None:
with self._lock:
self._state = new_state
def __repr__(self) -> str:
return f"Signal({self.direction}: {self._state.value})"
class Phase:
"""Defines which directions get green during this phase and for how long."""
def __init__(self, green_directions: set[str], green_duration: int, yellow_duration: int = 2):
self.green_directions = green_directions
self.green_duration = green_duration
self.yellow_duration = yellow_duration
self.total_duration = green_duration + yellow_duration
def __repr__(self) -> str:
return f"Phase(green={self.green_directions}, dur={self.green_duration}+{self.yellow_duration})"
class TimingStrategy(ABC):
"""Strategy for determining phase durations."""
@abstractmethod
def get_phases(self) -> list[Phase]: ...
class FixedTimingStrategy(TimingStrategy):
"""Fixed-cycle timing. Same durations every cycle."""
def __init__(self, ns_green: int = 30, ew_green: int = 25, yellow: int = 3):
self._ns_green = ns_green
self._ew_green = ew_green
self._yellow = yellow
def get_phases(self) -> list[Phase]:
return [
Phase({"NORTH", "SOUTH"}, self._ns_green, self._yellow),
Phase({"EAST", "WEST"}, self._ew_green, self._yellow),
]
class RushHourTimingStrategy(TimingStrategy):
"""Longer green for the main road during rush hour."""
def __init__(self, main_green: int = 45, cross_green: int = 15, yellow: int = 3):
self._main_green = main_green
self._cross_green = cross_green
self._yellow = yellow
def get_phases(self) -> list[Phase]:
return [
Phase({"NORTH", "SOUTH"}, self._main_green, self._yellow),
Phase({"EAST", "WEST"}, self._cross_green, self._yellow),
]
class SignalObserver(ABC):
"""Observer notified on signal state changes."""
@abstractmethod
def on_signal_change(self, direction: str, old_state: SignalState, new_state: SignalState) -> None: ...
class PedestrianCrossingObserver(SignalObserver):
def on_signal_change(self, direction: str, old_state: SignalState, new_state: SignalState) -> None:
if new_state == SignalState.RED:
print(f" [Pedestrian] Walk signal ON for crossing at {direction}")
elif old_state == SignalState.RED and new_state == SignalState.GREEN:
print(f" [Pedestrian] Walk signal OFF for crossing at {direction}")
class EmergencyObserver(SignalObserver):
def on_signal_change(self, direction: str, old_state: SignalState, new_state: SignalState) -> None:
if new_state == SignalState.GREEN:
print(f" [Emergency] {direction} is clear for emergency vehicles")
class Intersection:
"""Groups multiple signals into a coordinated unit."""
DIRECTIONS = ("NORTH", "SOUTH", "EAST", "WEST")
def __init__(self):
self.signals: dict[str, Signal] = {d: Signal(d) for d in self.DIRECTIONS}
self._observers: list[SignalObserver] = []
def add_observer(self, observer: SignalObserver) -> None:
self._observers.append(observer)
def _notify(self, direction: str, old_state: SignalState, new_state: SignalState) -> None:
for obs in self._observers:
obs.on_signal_change(direction, old_state, new_state)
def set_phase(self, green_directions: set[str]) -> None:
"""Set specified directions to GREEN, everything else to RED."""
for direction, signal in self.signals.items():
old = signal.state
if direction in green_directions:
signal.state = SignalState.GREEN
else:
signal.state = SignalState.RED
if signal.state != old:
self._notify(direction, old, signal.state)
def set_yellow(self, directions: set[str]) -> None:
"""Transition specified directions to YELLOW."""
for direction in directions:
signal = self.signals[direction]
old = signal.state
signal.state = SignalState.YELLOW
if old != SignalState.YELLOW:
self._notify(direction, old, SignalState.YELLOW)
def set_all_red(self) -> None:
for direction, signal in self.signals.items():
old = signal.state
signal.state = SignalState.RED
if old != SignalState.RED:
self._notify(direction, old, SignalState.RED)
def status(self) -> dict[str, str]:
return {d: s.state.value for d, s in self.signals.items()}
class TrafficController:
"""Coordinates signal phases for an intersection."""
def __init__(self, timing_strategy: TimingStrategy | None = None):
self._intersection = Intersection()
self._strategy = timing_strategy or FixedTimingStrategy()
self._phases = self._strategy.get_phases()
self._current_phase_idx = 0
self._tick_in_phase = 0
self._emergency_active = False
self._emergency_direction: str | None = None
# Start first phase
phase = self._phases[self._current_phase_idx]
self._intersection.set_phase(phase.green_directions)
@property
def intersection(self) -> Intersection:
return self._intersection
def add_observer(self, observer: SignalObserver) -> None:
self._intersection.add_observer(observer)
def tick(self) -> str | None:
"""Advance the controller by one time unit. Returns event description if a transition happened."""
if self._emergency_active:
return None # Hold current override until released
phase = self._phases[self._current_phase_idx]
self._tick_in_phase += 1
# Green phase ends, transition to yellow
if self._tick_in_phase == phase.green_duration:
self._intersection.set_yellow(phase.green_directions)
return f"Phase {self._current_phase_idx}: YELLOW for {phase.green_directions}"
# Yellow phase ends, advance to next phase
if self._tick_in_phase >= phase.total_duration:
self._tick_in_phase = 0
self._current_phase_idx = (self._current_phase_idx + 1) % len(self._phases)
next_phase = self._phases[self._current_phase_idx]
self._intersection.set_phase(next_phase.green_directions)
return f"Phase {self._current_phase_idx}: GREEN for {next_phase.green_directions}"
return None
def trigger_emergency(self, direction: str) -> None:
"""Override: set one direction green, all others red."""
print(f" ** EMERGENCY OVERRIDE: clearing {direction} **")
self._emergency_active = True
self._emergency_direction = direction
self._intersection.set_all_red()
self._intersection.set_phase({direction})
def release_emergency(self) -> None:
"""Resume normal cycling from the current phase."""
print(" ** EMERGENCY RELEASED: resuming normal cycle **")
self._emergency_active = False
self._emergency_direction = None
self._tick_in_phase = 0
phase = self._phases[self._current_phase_idx]
self._intersection.set_phase(phase.green_directions)
def set_timing_strategy(self, strategy: TimingStrategy) -> None:
"""Swap timing at runtime. Takes effect at the next phase boundary."""
self._strategy = strategy
self._phases = strategy.get_phases()
self._current_phase_idx = self._current_phase_idx % len(self._phases)
def status(self) -> dict[str, str]:
return self._intersection.status()
if __name__ == "__main__":
print("=== Traffic Signal Controller Demo ===\n")
controller = TrafficController(FixedTimingStrategy(ns_green=5, ew_green=4, yellow=2))
controller.add_observer(PedestrianCrossingObserver())
print("Initial state:", controller.status())
print("\n--- Running through two full cycles ---")
for t in range(28):
event = controller.tick()
if event:
print(f" Tick {t + 1}: {event}")
print(f" Status: {controller.status()}")
print("\n--- Emergency vehicle approaching from EAST ---")
controller.trigger_emergency("EAST")
print(f" Status: {controller.status()}")
# Hold emergency for a few ticks
for t in range(3):
controller.tick()
controller.release_emergency()
print(f" Status: {controller.status()}")
print("\n--- Switching to rush hour timing ---")
controller.set_timing_strategy(RushHourTimingStrategy(main_green=6, cross_green=3, yellow=2))
for t in range(20):
event = controller.tick()
if event:
print(f" Tick {t + 1}: {event}")
print(f" Status: {controller.status()}")
print("\nDone.")Interview Grading by Level
What an interviewer at each level expects to see in your answer. Use this to calibrate, not to perform.
Junior Engineer (L3)
Builds a four-way cycle with red, yellow, green, but signals self-transition and timing is hardcoded.
- Models RED, YELLOW, GREEN as an enum, not as strings.
- Cycles through NS and EW phases.
- Includes a yellow transition between green and red.
- Handles the four directions (NORTH, SOUTH, EAST, WEST).
- Names State pattern when asked.
- Lets signals decide their own timing, so two perpendicular directions can theoretically go green together.
- Hardcodes durations inside Signal or Intersection rather than externalizing them.
- Skips the yellow phase, jumping green directly to red.
- Forgets the emergency override entirely.
- Encodes the safety invariant as comments rather than enforcing it through centralized control.
Mid-Level Engineer (L4)
Drives all transitions from a single TrafficController, uses TimingStrategy for swappable phase durations, and implements emergency override correctly.
- Centralizes all signal transitions in TrafficController; signals are passive.
- Writes Phase as a data structure with explicit green and yellow durations.
- Implements TimingStrategy as an interface with Fixed and RushHour as concrete strategies.
- Handles emergency override by setting all red first, then setting the requested direction green.
- Resumes the normal cycle from the current phase index after emergency release, not from phase 0.
- Uses Observer for pedestrian crossings and emergency hardware notifications.
- Does not volunteer left-turn protected phases as the natural next extension.
- Treats controller crash as out of scope rather than naming flash-to-red as the safe failure mode.
- Misses adaptive timing (sensor-driven phase durations) as the Strategy extension point.
Senior Engineer (L5+)
Volunteers extensions and failure modes, frames each pattern around the bug it prevents, and names the safety invariant in plain words.
- Volunteers left-turn protected phases, adaptive (sensor-driven) timing, and pedestrian crossing button extensions without prompting.
- Names the safety invariant: at most one set of non-conflicting directions green at any time, enforced by centralized control, never by distributed agreement.
- Frames each pattern around the failure it prevents: State enum stops invalid color transitions, Strategy decouples timing config from controller logic, Observer keeps emergency preemption hardware out of the controller.
- Defends Strategy over constants by naming AdaptiveTimingStrategy concretely (reads loop detector queue lengths, adjusts green duration proportionally).
- Proposes flash-to-red as the watchdog-triggered failure mode with a clear semantic (treat as four-way stop).
- Acknowledges left-turn phases as the realistic next scope expansion and notes that Phase as data, not code branches, is what makes the extension cheap.
- Closes with a one-sentence summary that names the State-Strategy-Observer trio and the no-conflicting-greens invariant in under 20 seconds.
Common Mistakes
- ✗Letting signals self-transition without a controller. Two perpendicular signals can both go green, causing a simulated collision.
- ✗Using string comparisons for signal states instead of enums. 'GRREN' won't cause a compile error, but it will cause a runtime bug.
- ✗Hardcoding timing durations inside the signal. Now changing rush hour timing means editing signal internals.
- ✗Forgetting the yellow phase. Jumping straight from green to red is unrealistic and breaks any simulation that models driver behavior.
Key Points
- ✓State pattern eliminates if/else chains for phase transitions. Each state knows its successor, so invalid transitions are structurally impossible.
- ✓Signals are passive. They don't decide when to change. The controller tells them. This prevents two conflicting directions from going green.
- ✓Timing config as a separate object means rush hour, nighttime, and adaptive schedules are just data swaps, not code changes.
- ✓Observer for emergency vehicles keeps priority override decoupled from normal signal logic.