Low Level Design
Design a Traffic Signal Controller
A complete low-level design walkthrough for a traffic signal control system — State pattern for signal transitions, Singleton context as the FSM, and sensor-driven adaptive durations with emergency override.
Problem Description#
Design a Traffic Signal Controller that manages signal transitions at an intersection — cycling through Red → Yellow → Green — with durations that adapt to real-time traffic levels and can respond to emergency vehicles.
At its core, a traffic light is a finite state machine:
- The system is always in exactly one state (Red, Yellow, or Green)
- Each state has its own duration and knows which state comes next
- Duration is not fixed — it adjusts based on a sensor reading (LOW / MEDIUM / HIGH / EMERGENCY)
- An emergency overrides the normal cycle and forces immediate Green
This problem is a textbook application of the State pattern: behavior changes based on current state, and adding a new state (e.g., Flashing Yellow) requires zero changes to existing code.
Clarify Requirements#
Before designing, ask these in an interview:
Functional
- What states does the signal support? (Red, Yellow, Green — any others like flashing?)
- Are durations fixed or traffic-adaptive?
- How is traffic level detected — sensor, API, manual input?
- How should emergency vehicles be handled — immediate green? Priority lane?
- Should the system support multi-intersection coordination?
Non-functional
- Does the controller run indefinitely or for a fixed number of cycles?
- Is thread safety required (multiple controllers per intersection)?
- Should state transitions be logged or auditable?
Final Requirements#
After clarification, here's what we'll build:
- Three states: Red, Yellow, Green — each encapsulates its own duration and next-state logic
- TrafficSensor simulates live traffic readings: LOW (2s), MEDIUM (3s), HIGH (4s), EMERGENCY (override)
- On EMERGENCY: whichever state detects it transitions immediately to GreenLightState
- TrafficSignalContext is a Singleton FSM: holds current state, delegates timing to it, then calls getNextState() for the transition
- The signal loop runs indefinitely — each cycle: setDuration() → sleep() → transition
Core Entities#
| Entity | Responsibility |
|---|---|
| TrafficState | Interface — getDuration(), setDuration(), getNextState(), setContext() |
| RedLightState | Stop phase; next → Yellow; duration 2–4s; EMERGENCY → immediate Green |
| YellowLightState | Caution phase; next → Green; duration 2–4s; EMERGENCY → immediate Green |
| GreenLightState | Go phase; next → Red; duration 2–5s; EMERGENCY extends to 5s |
| TrafficSensor | Simulates a sensor reading — returns LOW / MEDIUM / HIGH / EMERGENCY randomly |
| TrafficSignalContext | Singleton FSM — holds current state, drives the signal loop, exposes setState() for emergency override |
Patterns Used#
1. State Pattern — TrafficState / RedLightState / YellowLightState / GreenLightState#
The State pattern is the core of this design. Instead of a single class with a giant if/else or switch on the current signal colour, each state is its own class. It knows:
- How long it should last (setDuration reads the sensor and sets time)
- What state follows it (getNextState)
- What to do on EMERGENCY (override the context's state directly via context.setState())
Adding a FlashingYellowState or PedestrianCrossingState is a pure addition — no existing state class changes.
2. Singleton — TrafficSignalContext#
There is exactly one intersection controller. TrafficSignalContext.getInstance() uses a synchronized factory method for thread-safe lazy initialization. The single instance is the authority over which state is active and drives the infinite signal loop.
3. Context as Finite State Machine#
TrafficSignalContext.startTrafficSignal() is the FSM engine:
loop:
currentState.setDuration() ← sensor-driven, may override state
sleep(currentState.getDuration())
transition to currentState.getNextState()
Each state can short-circuit the normal transition by calling context.setState(new GreenLightState()) directly — this is the emergency-vehicle override path.
Code#
State Interface#
TrafficState defines the full contract. setContext lets states call back to the FSM for emergency overrides.
public interface TrafficState {
int getDuration();
void setDuration() throws InterruptedException;
TrafficState getNextState();
void setContext(TrafficSignalContext context);
}
Concrete States#
Each state reads the sensor in setDuration() and adjusts its timer. EMERGENCY triggers an immediate context transition to Green.
class RedLightState implements TrafficState {
private int time = 3;
private TrafficSignalContext context;
@Override public TrafficState getNextState() { return new YellowLightState(); }
@Override public void setContext(TrafficSignalContext c){ this.context = c; }
@Override public int getDuration() { return time; }
@Override
public void setDuration() {
String level = new TrafficSensor().getTrafficLevel();
switch (level) {
case "LOW" -> this.time = 2;
case "MEDIUM" -> this.time = 3;
case "HIGH" -> this.time = 4;
case "EMERGENCY" -> {
System.out.println("🚨 Emergency detected at Red — switching to Green!");
context.setState(new GreenLightState());
}
default -> System.out.println("No sensor data — using default duration.");
}
}
}class YellowLightState implements TrafficState {
private int time = 2;
private TrafficSignalContext context;
@Override public TrafficState getNextState() { return new GreenLightState(); }
@Override public void setContext(TrafficSignalContext c){ this.context = c; }
@Override public int getDuration() { return time; }
@Override
public void setDuration() {
String level = new TrafficSensor().getTrafficLevel();
switch (level) {
case "LOW" -> this.time = 2;
case "MEDIUM" -> this.time = 3;
case "HIGH" -> this.time = 4;
case "EMERGENCY" -> {
System.out.println("🚨 Emergency during Yellow — switching to Green!");
context.setState(new GreenLightState());
}
default -> System.out.println("No sensor data — using default duration.");
}
}
}class GreenLightState implements TrafficState {
private int time = 4;
private TrafficSignalContext context;
@Override public TrafficState getNextState() { return new RedLightState(); }
@Override public void setContext(TrafficSignalContext c){ this.context = c; }
@Override public int getDuration() { return time; }
@Override
public void setDuration() {
String level = new TrafficSensor().getTrafficLevel();
switch (level) {
case "LOW" -> this.time = 2;
case "MEDIUM" -> this.time = 3;
case "HIGH" -> this.time = 4;
case "EMERGENCY" -> {
System.out.println("🚨 Emergency — already Green, extending to 5s.");
this.time = 5;
}
default -> System.out.println("No sensor data — using default duration.");
}
}
}Sensor & Context#
TrafficSensor simulates a live reading. TrafficSignalContext is the Singleton FSM — it owns the state and drives the loop.
class TrafficSensor {
public String getTrafficLevel() {
return switch ((int)(Math.random() * 4)) {
case 0 -> "LOW";
case 1 -> "MEDIUM";
case 2 -> "HIGH";
default -> { System.out.println("🚨 Emergency vehicle detected!"); yield "EMERGENCY"; }
};
}
}import java.util.Objects;
public class TrafficSignalContext {
private static TrafficSignalContext context;
private TrafficState currentState;
private TrafficSignalContext() {
currentState = new RedLightState();
currentState.setContext(this);
}
public static synchronized TrafficSignalContext getInstance() {
if (context == null) context = new TrafficSignalContext();
return context;
}
/** Infinite FSM loop: set duration → sleep → transition. */
public void startTrafficSignal() throws InterruptedException {
while (true) {
currentState.setDuration(); // sensor read + possible emergency override
System.out.println("🚦 " + currentState.getClass().getSimpleName()
+ " | " + currentState.getDuration() + "s");
Thread.sleep(currentState.getDuration() * 1000L);
setState(currentState.getNextState());
}
}
/** Used by states for emergency override; ignores no-op transitions. */
public void setState(TrafficState state) {
if (state != null && !Objects.equals(currentState.getClass(), state.getClass())) {
this.currentState = state;
this.currentState.setContext(this);
}
}
}Demo#
public class TrafficSignalControl {
public static void main(String[] args) throws InterruptedException {
TrafficSignalContext context = TrafficSignalContext.getInstance();
context.startTrafficSignal();
// Output (example):
// 🚦 RedLightState | 3s
// 🚦 YellowLightState | 2s
// 🚨 Emergency vehicle detected!
// 🚨 Emergency during Yellow — switching to Green!
// 🚦 GreenLightState | 4s
// 🚦 RedLightState | 2s ← LOW traffic
}
}
Class Diagram#
Extendible — Follow Ups#
1. Add a Flashing Yellow (night mode) state#
Create FlashingYellowState implements TrafficState — its getNextState() returns itself and its setDuration() ignores the sensor. Wire it in by calling context.setState(new FlashingYellowState()) at night. Zero changes to existing states.
2. Multi-intersection coordination#
Replace the single TrafficSignalContext with an IntersectionController that owns four TrafficSignalContext instances (one per road). It enforces the constraint that at most one road is green at a time using a shared lock or a coordinating state machine.
3. Observer for state-change events#
Add a TrafficStateObserver interface with onStateChange(TrafficState state). TrafficSignalContext.setState() notifies all registered observers — useful for display boards, logging services, or an emergency-vehicle dispatch system.
4. Configurable durations via strategy#
Extract duration logic out of each state into a DurationStrategy interface (int computeDuration(String trafficLevel)). Inject it per state — swap FixedDurationStrategy in tests and SensorDurationStrategy in production without changing any state class.
5. Real sensor integration#
Replace TrafficSensor's random number with a call to an HTTP sensor API or a message queue consumer. Since TrafficSensor is already isolated behind its own class boundary, the change is confined to one class.
6. Audit trail / event sourcing#
Log every state transition — timestamp, previous state, new state, trigger (normal or emergency) — to an append-only TransitionLog. Replay the log to reconstruct intersection history for traffic engineering analysis.