Background & the problem
The problem with flat state machines
A finite-state machine has one active state at a time and a flat list of transitions. That works until it doesn't: real systems accumulate modes, sub-modes, and concurrent activities, and a flat machine answers with an explosion of states and duplicated transitions.
Three things go wrong as a flat FSM grows:
- Duplicated transitions. A "cancel" event valid in ten states means ten identical transitions. Add an eleventh state, forget the edge, ship a bug.
- State explosion under concurrency. Two independent toggles (A on/off, B on/off) need four states. Three need eight. The cross-product is exponential.
- Lost context on re-entry. Leave a multi-step flow to answer a notification, come back — a flat machine starts you over from step one.
Statecharts: the honest model
A statechart, introduced by David Harel, adds three constructs that keep the model honest as it grows:
- Hierarchy. A state can contain sub-states. A transition on the parent applies no matter which child is active, so you write it once. (Solves duplication.)
- Parallel regions. A state can hold several independent regions that are all active at once, each with its own current state. This replaces the cross-product. (Solves explosion.)
- History. Re-entering a compound state can return to the sub-state it was in when last left, rather than starting over. (Solves lost context.)
fate implements all three — plus guards, entry/exit and transition actions, final states, delayed transitions, and invoked work.
Why another statechart library?
The ideas aren't new (SCXML, XState). What was missing was a Go engine that is:
| Goal | How fate meets it |
|---|---|
| Embeddable anywhere | Zero dependencies — the engine is standard-library-only, so adopting it never drags in a web server or an SDK. |
| Replay-safe | The engine performs no side effects, so the same machine + events always produce the same state, byte-for-byte. Durable runtimes like Temporal replay workflow code and require exactly this. |
| Typed | Context and events are your Go types via generics, checked at compile time. |
| Inspectable | Render to ASCII / Mermaid / graph JSON, diff two machines, and drive any machine live in the studio. |
The one idea worth internalising
The engine computes state. Adapters perform effects.
The engine never reads the clock, sleeps, calls the network, or touches the filesystem. When a machine wants to wait or call a service, the engine records that intent as data (a pending timer, a pending invocation) and waits to be told the outcome. A component called an adapter reads those pending effects and performs them.
That single rule is what lets the same machine run in a microsecond-fast unit test with no clock, and inside a Temporal workflow for three weeks — producing identical transitions. How it works makes it concrete.
Next
- Getting started — your first machine in a few lines.
- Concepts — the active configuration,
MachinevsActor.