Skip to content
Ocean-Atlasv0.1.0Canonical Knowledge

Entity-Backed FSM

A persistent entity — an order, an invoice, a shipment — has a well-defined lifecycle: a limited set of valid states and transitions, and every transition must update the stored record.

How do you centralize the permitted state transitions for a persistent entity without scattering status-check if logic across every service method that might change it?

  • Consistency — the entity’s persisted status and its in-memory transition must never disagree.
  • Coupling — the FSM needs to know which persisted field it controls, without owning the entity’s other persistence concerns (queries, indexes).
  • Reuse — the same entity may be read through several unrelated database operations that must not bypass the FSM’s transition rules.
  • Complexity — entry/exit rules (timestamps, computed fields) need one defined place to live, not one per caller.

Declare an @fsm that controls one persisted field of a @database entity (controls Order.status), keyed by the entity’s identity. Declare each valid event, and per-state on event rules using next <State> : <field assignment>. Treat the FSM as the ownership boundary for the controlled field: API and service operations should trigger FSM events instead of exposing database commands that modify that field directly. Other database queries and commands remain free to read and write the rest of the entity.

stateDiagram-v2
[*] --> New
New --> Approved: approve
New --> Cancelled: cancel
Approved --> Shipped: ship
Approved --> Cancelled: cancel
Shipped --> Delivered: deliver
Delivered --> [*]
Cancelled --> [*]
@database
Database OrderDb
engine = postgres
configType = DatabaseConfig
Entity Order
key(id)
query findById(id: String) : Order
command T updateOrder(order: Order) : Order
@fsm
OrderFSM controls Order.status
key: id
event approve in(_) out(order:Order)
event ship in(_) out(order:Order)
event cancel in(_) out(order:Order)
state New
on event approve:
next Approved : this.updatedAt = nowUTC()
on event cancel:
next Cancelled : this.updatedAt = nowUTC()
state Approved
on event ship:
next Shipped : this.updatedAt = nowUTC()
state Shipped
state Delivered
state Cancelled

This is a minimal illustrative fragment, not a complete duplicated example — see Working Examples for the full, runnable model.

  • Transition rules live in one place, independent of every API or service that might trigger them.
  • Unsupported transitions (for example, Delivered back to Shipped) are absent from the FSM’s event rules rather than guarded by scattered conditionals.
  • Per-transition field updates are explicit and auditable.
  • The FSM only controls the one field it declares; broader entity consistency (other fields, related entities) remains the service’s and database’s responsibility.
  • The model must avoid alternate database commands that bypass the FSM and modify the controlled field directly unless the active toolchain explicitly enforces exclusive ownership.
  • Adding a new state or event means touching the FSM definition — a single coupling point, by design.
  • If the behavior has no persisted identity worth controlling (a transient calculation session), use In-Memory FSM instead.
  • If the transitions are really human approve/reject decisions with a recorded actor, see Approval Workflow, which specializes this pattern.
  • In-Memory FSM — the alternative when there is no persisted entity to control.
  • Approval Workflow — a specialization for human decision-driven transitions.
  • Database-Backed CRUD — the free-form persistence and API surface this pattern constrains for one field.