Building a Traffic Light System
There are a number of popular web architecture questions that companies like to ask candidate, one of which is the traffic light question. Let us dive down the rabbit hole once again and unravel this prompt together.
Table of Contents
Overview
There's a number of popular web architecture questions that companies like to ask candidates. One I've covered previously on my blog is the infamous elevator question, but there's another I've heard often, and that's the traffic light exercise. Let's dive down the rabbit hole once again and unravel this prompt, spending way more time than we should building a working prototype.
Disclaimer: The scenario in this post is made up. If you end up in a situation similar to one outlined I'm sorry, but this post is not the post you're looking for. Don't use any of the code in this post to write actual software for traffic lights, please.
Places, Please...
You've found yourself working at an enterprise transport solutions company as a Software Engineer. The company bought you on board as they just signed a contract with the City of New York to write a new traffic light system. The city terminated their agreement with the previous provider and in two weeks time every traffic light in New York City will need to have the software you haven't written yet implemented or else the city will plummet into absolute chaos. Despite being a web focused JavaScript engineer you've been assigned the task with little guidance.
Undeterred by your lack of experience with traffic lights and a desire to not be arrested for negligence you set out to build a scalable system. For this task we'll consider the following:
- There needs to be lights for pedestrians to indicate when they should cross and lights for cars to indicate when they should move or stop.
- The system needs to support multiple lights. Most traffic lights in New York City are part of intersections and there's usually a minimum of 4.
- There should be a number of safety checks and redundencies. The first rule of trafifc lights is that they should actively protect protect people instead of placing them in harms way.
Using a white board we can begin to imagine what this might look like. In my system diagram the center controller handles the system state, and each child node attached to it is a light which derives its state from the controller. Whenever a request is made to change the light pattern the color each light should change to is sent to each individual light node. The light node will then process and update its own color state internally. When the color is updated the node will then send back a verification signal to the controller so it knows when to flip the switch on the pedestrian lights. This vertification is done as a safety precaution to prevent a situation where a single light failed to update its internal state, causing the system to go out of sync.
I will admit my system diagram probably isn't the cleanest or easiest to read but hopefully it gets the message across.
Building a Proof of Concept
Now that we have our system diagram we can start translating it to code. For simplicity sake I'm going to use React and TypeScript for this, although much like the elevator exmaple these are for sure not the correct tools to use. In a real world situation you may use something like C or Assembly, or perhaps you wouldn't even use a programming language at all, who knows.
We'll start with the parameters for the system. For this example we'll model things around a typical American intersection which has roads going in a cross pattern. When one direction is red the other will be green, and pedestrians will be able to cross in the opposite direction. To get started we'll define a series of enum values that we can use to keep our referneces consistent along with the default state for the light system controller.
enum Color {
RED = 'RED',
AMBER = 'AMBER',
GREEN = 'GREEN',
}
enum Axis {
X = 'X',
Y = 'Y',
}
enum Direction {
NORTH = 'NORTH',
EAST = 'EAST',
SOUTH = 'SOUTH',
WEST = 'WEST',
}
const defaultState = {
[Axis.X]: {
[Direction.WEST]: {
status: Color.RED,
},
[Direction.EAST]: {
status: Color.RED,
},
},
[Axis.Y]: {
[Direction.NORTH]: {
status: Color.GREEN,
},
[Direction.SOUTH]: {
status: Color.GREEN,
},
},
}
The light controller component will hold onto the system state defaulting to the previously mentioned values. When a button is pressed from within the controller
it will update the requested state which will then be passed into each light component as the cycleToColor
prop.
const TrafficLightSystem = () => {
const [requestedLightStatus, setRequestedLightStatus] = useState(defaultState)
const handleClick = () => {
const determineNewLightStatus = (color: Color) =>
color === Color.GREEN ? Color.RED : Color.GREEN
const updatedRequestedLightStatus = {
[Axis.Y]: {
[Direction.NORTH]: {
status: determineNewLightStatus(
lightStatus[Axis.Y][Direction.NORTH].status,
),
},
[Direction.SOUTH]: {
status: determineNewLightStatus(
lightStatus[Axis.Y][Direction.SOUTH].status,
),
},
},
[Axis.X]: {
[Direction.WEST]: {
status: determineNewLightStatus(
lightStatus[Axis.X][Direction.WEST].status,
),
},
[Direction.EAST]: {
status: determineNewLightStatus(
lightStatus[Axis.X][Direction.EAST].status,
),
},
},
}
setRequestedLightStatus(updatedRequestedLightStatus)
}
return (
<>
<button onClick={handleClick}>Cycle Lights></button>
<TrafficLight
cycleToColor={requestedLightStatus[Axis.Y][Direction.NORTH].status}
/>
<TrafficLight
cycleToColor={requestedLightStatus[Axis.Y][Direction.SOUTH].status}
/>
<TrafficLight
cycleToColor={requestedLightStatus[Axis.X][Direction.WEST].status}
/>
<TrafficLight
cycleToColor={requestedLightStatus[Axis.X][Direction.EAST].status}
/>
</>
)
}
The TrafficLight
component handles the inner workings of the light change. Once it has recieved an updated cycleToColor
prop it will begin the light change cycle by making a call to handleLightCycle
within the useEffect
hook, resolving a promise once each color change has been completed.
const TrafficLight = ({ cycleToColor }: TrafficLightProps) => {
const [color, setColor] = useState(cycleToColor)
useEffect(() => {
const handleLightChange = (updatedColor: Color) =>
// Strictly for demonstration purposes...
new Promise((resolve, reject) => {
setTimeout(() => {
setColor(updatedColor)
resolve(true)
}, 2000)
})
const handleLightCycle = async () => {
await handleLightChange(Color.AMBER)
await handleLightChange(cycleToColor)
}
if (color !== cycleToColor && color !== Color.AMBER) {
// Prevents the light cycle being triggered again once the color is updated to Amber.
handleLightCycle()
}
}, [color, cycleToColor])
return (
<div className="traffic-light">
<span
className={`traffic-light__dome traffic-light__dome--red ${
color === Color.RED && 'traffic-light__dome--illuminated'
}
${error && 'traffic-light__dome--error'}
`}
></span>
<span
className={`traffic-light__dome traffic-light__dome--amber ${
color === Color.AMBER && 'traffic-light__dome--illuminated'
}`}
></span>
<span
className={`traffic-light__dome traffic-light__dome--green ${
color === Color.GREEN && 'traffic-light__dome--illuminated'
}`}
></span>
</div>
)
}
Fail Safes
While what we have technically works, the fail safes refereced in the system diagram are not implemented. Once the controller passes the requested color into each traffic light it has no idea if it was actually successful or not. Naturally this can result in a number of problems such as cars crashing into each other or pedestrians being run over, which is probably something we want to avoid.
To resolve this we'll extend the light system to include a lightStatus
state which is identical to requestedLightStatus
. We'll also pass each traffic light an onErrorDetected
and onCycleChange
callback.
Once the traffic light has finished changing its colors we'll set the lightStatus
state to indicate that it has finished via the onCycleChange
method. This essentially makes our state two way where the system controller requests a light color
and then each light verifies the change back to the system once it's been completed.
Likewise for the error state, if the promise doesn't resolve within handleLightChange
and it gets rejected we can handle the error by putting the entire system into an error mode. In the real world this
would be something like flashing red lights across the entire intersection.
const onErrorDetected = useCallback(() => {
setError(true)
}, [])
const onCycleChange = useCallback(
({ axis, direction, status }) => {
const statusClone = { ...lightStatus }
statusClone[axis][direction].status = status
setLightStatus(statusClone)
},
[setLightStatus, lightStatus],
)
const handleLightCycle = async () => {
try {
await handleLightChange(Color.AMBER)
onCycleChange({ axis, direction, status: Color.AMBER })
await handleLightChange(cycleToColor)
onCycleChange({ axis, direction, status: cycleToColor })
} catch (error) {
onErrorDetected()
}
}
As our system now verifies the color change we can use the lightStatus
state to toggle the pedestrian indicators once we've checked that the opposite directions lights are red.
const TrafficLightSystem = () => {
const [lightStatus, setLightStatus] = useState(defaultState)
const [requestedLightStatus, setRequestedLightStatus] = useState(defaultState)
const [error, setError] = useState(false)
const handleClick = () => {
const determineNewLightStatus = (color: Color) =>
color === Color.GREEN ? Color.RED : Color.GREEN
const updatedRequestedLightStatus = {
[Axis.Y]: {
[Direction.NORTH]: {
status: determineNewLightStatus(
lightStatus[Axis.Y][Direction.NORTH].status,
),
},
[Direction.SOUTH]: {
status: determineNewLightStatus(
lightStatus[Axis.Y][Direction.SOUTH].status,
),
},
},
[Axis.X]: {
[Direction.WEST]: {
status: determineNewLightStatus(
lightStatus[Axis.X][Direction.WEST].status,
),
},
[Direction.EAST]: {
status: determineNewLightStatus(
lightStatus[Axis.X][Direction.EAST].status,
),
},
},
}
setRequestedLightStatus(updatedRequestedLightStatus)
}
const onErrorDetected = useCallback(() => {
setError(true)
}, [])
const onCycleChange = useCallback(
({ axis, direction, status }) => {
const statusClone = { ...lightStatus }
statusClone[axis][direction].status = status
setLightStatus(statusClone)
},
[setLightStatus, lightStatus],
)
return (
<>
<button onClick={handleClick}>Cycle Lights</button>
<PedestrianLights
axis={Axis.Y}
walk={
lightStatus[Axis.Y][Direction.NORTH].status === Color.RED &&
lightStatus[Axis.Y][Direction.SOUTH].status === Color.RED
}
/>
<PedestrianLights
axis={Axis.X}
walk={
lightStatus[Axis.X][Direction.EAST].status === Color.RED &&
lightStatus[Axis.X][Direction.WEST].status === Color.RED
}
/>
<TrafficLight
axis={Axis.Y}
cycleToColor={requestedLightStatus[Axis.Y][Direction.NORTH].status}
direction={Direction.NORTH}
error={error}
onCycleChange={onCycleChange}
onErrorDetected={onErrorDetected}
/>
<TrafficLight
axis={Axis.Y}
cycleToColor={requestedLightStatus[Axis.Y][Direction.SOUTH].status}
direction={Direction.SOUTH}
error={error}
onCycleChange={onCycleChange}
onErrorDetected={onErrorDetected}
/>
<TrafficLight
axis={Axis.X}
cycleToColor={requestedLightStatus[Axis.X][Direction.WEST].status}
direction={Direction.WEST}
error={error}
onCycleChange={onCycleChange}
onErrorDetected={onErrorDetected}
/>
<TrafficLight
axis={Axis.X}
cycleToColor={requestedLightStatus[Axis.X][Direction.EAST].status}
direction={Direction.EAST}
error={error}
onCycleChange={onCycleChange}
onErrorDetected={onErrorDetected}
/>
</>
)
}
Simulation
You can check out my traffic light simulation below by clicking the button to begin the light cycle change, or alternatively you can use the other button to toggle the error state. Although I used React for this project I tried to keep the examples straight forward enough so it translates well regardless of what framework you're using.
You're Hired
Hopefully this article helps prepare you if you end up getting this question. While the code examples are not the cleanest in the world I hope it at least gives you an understanding of how you could build such a system. It's worth mentioning that this post vastly over simplifies the problems that actual traffic engineers encounter (I hope).
As a side note I was recently in New York City which was the inspiration behind this post. I really enjoyed taking photos while I was there, some of which I've attached below. 🎉