Skip to content

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:

  1. Duplicated transitions. A "cancel" event valid in ten states means ten identical transitions. Add an eleventh state, forget the edge, ship a bug.
  2. State explosion under concurrency. Two independent toggles (A on/off, B on/off) need four states. Three need eight. The cross-product is exponential.
  3. 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:

GoalHow fate meets it
Embeddable anywhereZero dependencies — the engine is standard-library-only, so adopting it never drags in a web server or an SDK.
Replay-safeThe 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.
TypedContext and events are your Go types via generics, checked at compile time.
InspectableRender 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

Released under the MIT License · v0.4.0 · pkg.go.dev