Skip to content

Hierarchical Traffic Light

A traffic light that demonstrates the core tradeoff in hierarchical state machines: you can model the same behavior with a flattened machine (all states at the top level, explicit transitions everywhere) or a nested machine (child states inherit transitions from parents via propagation).

WorkingrepairmaintenancebreakmaintenancecompleteBrokenRedGreenYellowMaintenance
WorkingRed
import { defineStates, matchina } from "matchina";
import { submachine, nestedHsmRoot } from "matchina/hsm";
// 1. Define the Child Machine (Light Cycle)
// We need a factory for the child machine so it can be instantiated freshly
const lightCycleStates = defineStates({
Red: undefined,
Green: undefined,
Yellow: undefined,
});
const createLightCycle = () =>
matchina(
lightCycleStates,
{
Red: { tick: "Green" },
Green: { tick: "Yellow" },
Yellow: { tick: "Red" },
},
lightCycleStates.Red()
);
// 2. Define the Parent Machine (Controller)
// We use `submachine` to embed the child machine factory
const controllerStates = defineStates({
Broken: undefined,
Working: submachine(createLightCycle),
Maintenance: undefined,
});
const createController = () =>
matchina(
controllerStates,
{
Broken: { repair: "Working", maintenance: "Maintenance" },
Working: { break: "Broken", maintenance: "Maintenance" },
Maintenance: { complete: "Working" },
},
controllerStates.Working()
);
// 3. Create the Hierarchical Machine
// This wraps the controller with propagation logic
export function createPropagatingTrafficLight() {
const root = createController();
return nestedHsmRoot(root);
}
repairmaintenancebreakmaintenancecompleteBrokenWorkingMaintenance
WorkingRed
import { createHSM } from "matchina/hsm";
export function createFlatTrafficLight() {
return createHSM({
initial: "Working",
states: {
Broken: {
on: {
repair: "Working",
maintenance: "Maintenance",
},
},
// Working is a hierarchical state with light cycle substates
Working: {
initial: "Red",
states: {
Red: {
on: {
tick: "Green",
},
},
Green: {
on: {
tick: "Yellow",
},
},
Yellow: {
on: {
tick: "Red",
},
},
},
// Parent-level transitions apply to all child states
on: {
break: "^Broken",
maintenance: "^Maintenance",
},
},
Maintenance: {
on: {
complete: "Working",
},
},
},
});
}

In the nested variant, child states like Pedestrian.Walk and Pedestrian.Flashing automatically inherit the reset transition defined on their parent. You define shared behavior once on the parent, and it propagates down.

In the flattened variant, every state is a peer at the top level. Each state that needs to respond to reset must declare that transition explicitly. The machine is more verbose but immediately readable — no implicit inheritance to reason about.

Neither is universally better. Use nesting when you have genuine shared transitions across several child states. Use flat machines when clarity and explicitness matter more than concision.