Skip to main content
When your business objects have non-trivial lifecycles — an order goes through cart → checkout → paid → fulfilled → shipped → delivered → disputed — modeling each as an explicit finite state machine saves you from a class of bugs that otherwise dominate your codebase. This pattern works well on top of event-driven or queue-backed handlers: webhooks trigger state transitions, and transitions are validated explicitly.

The shape

Every state change is an explicit function. You know exhaustively what can happen next at any state.

When it’s the right fit

  • Multi-step commerce flows — carts, checkouts, orders, shipments
  • Long-lived agent interactions — an agent’s task progresses through assigned → accepted → working → delivered → rated
  • Regulated flows — every state change is also a durable audit event
  • Mandate lifecyclesactive → paused → revoked
  • Anything where “what state are we in?” matters to correctness
Skip this pattern for ephemeral or stateless work (idempotent upserts, analytics fan-out).

Define states explicitly

Benefits:
  • Invalid transitions (cart → shipped) throw immediately
  • Visualizable — xstate has a visualizer that renders the diagram
  • Serializable — store state as a column; restore by loading it
  • Exhaustive — you can’t forget to handle a state

Wire webhooks to transitions

Audit trail for free

Every transition is a row in order_events. That’s your audit log. For regulated flows, this becomes the compliance record:
Returns the full history: who moved to what state, when, and triggered by which webhook event. Preserved forever; never delete.

Side effects as enqueued jobs

Don’t execute side effects inline — enqueue them. The state transition itself is the durable signal:
Benefits:
  • Side effects run asynchronously; state transition is durable
  • Each side effect has its own retry / DLQ
  • New side effects don’t require changing the transition logic

Compose with agent state

If you’re building agents, every agent is itself an FSM:
And each A2A task has its own state machine:
Model these explicitly. The Sly platform tracks its own internal state; your state mirrors it, with your business events layered on.

Handling concurrency

Two webhooks for the same order arriving simultaneously (common after a burst) → use optimistic locking:
The version column on your table guarantees no two concurrent transitions corrupt the state.

Testing

FSMs are testable by enumerating transitions:
Full coverage of state transitions is achievable in a way raw webhook handlers rarely achieve.

Downsides

  • Upfront design cost — FSM has to be right; refactoring states later is painful
  • Over-engineering risk — a 3-state CRUD object doesn’t need this
  • Library dependency — xstate (Node) or similar is an additional dependency; some teams prefer rolling their own state tables
All support visualization, persistence, guards, and side-effect hooks.

Observability

  • State transition histogram per day / per object type
  • Time-in-state — long-lived awaiting_payment = checkout abandonment; long fulfilling = shipping issue
  • Invalid-transition-attempted counter — high count = webhook-handler bug
  • State distribution — snapshot of where your orders are right now
Each of these drives product insight beyond what a raw-DB view gives you.

See also