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.

Published 1 year ago
9 min read
976 views

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.

Lets say I tasked you with building a traffic light system... how would you do that?
I did not realize this company made traffic... you know what, sure... ok...

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.

New York City Police Car (Smart Car)

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:

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.

System Diagram explaining component
archtecture

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.

Pedestrian traffic
lights

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

China Town
Mural

Pizza
Mural

James in front of
graffiti

Building mural