State Machine Notes
Core Elements
| Element |
Description |
| State |
A condition the system is in (e.g. Draft, Published) |
| Transition |
An arrow from one state to another, triggered by an event |
| Event |
What causes the transition (e.g. publish, cancel) |
| Guard |
A condition that must be true for a transition to fire (e.g. [price > 0]) |
| Action |
What happens during a transition (e.g. / send_email) |
| Initial state |
Filled black circle — where the machine starts |
| Final state |
Black circle with a ring — terminal condition |
Events
An event triggers a state transition. Syntax:
EventName [Guard] / Action
Types of Events
| Type |
Description |
Example |
| Command |
An explicit request from a user/system |
publish, cancel_order |
| Time |
Triggered by time passing |
after(30 days) |
| Condition |
Triggered when a condition becomes true |
when(stock == 0) |
| Internal |
Triggers an action without changing state |
/ log_view |
In code, events map to
- An API endpoint (
PUT /products/:id/publish)
- A use case (
PublishProductUseCase)
- Or a method on the model with a state field (
product.Publish())
Guards (if/else equivalent)
No if/else in state machines — use guarded parallel transitions instead.
Instead of if/else:
on publish:
if price > 0:
go to Active
else:
go to Rejected
Model as two guarded transitions:
Draft ──publish [price > 0]──────→ Active
Draft ──publish [price == 0]─────→ Rejected
Priority / else-equivalent:
Draft ──publish [price > 0 AND settings == 100]──→ Active (1st)
Draft ──publish [else]───────────────────────────→ Rejected (2nd / default)
Mental shift
| Imperative (if/else) |
State machine |
| You write the flow |
You define the rules, the machine enforces them |
| Conditions buried in code |
Conditions visible on the diagram |
| Easy to add invalid paths |
Invalid transitions simply don't exist |
Example — Product Lifecycle
[●] → Draft ──publish──→ Active ──archive──→ Archived
│ │
delete suspend
↓ ↓
Deleted Suspended ──restore──→ Active
With guards and actions:
Draft ──publish [price > 0 AND split_settings total == 100]──→ Active
/ changelog.LogUpdate()
Active ──mark_sold_out──→ SoldOut
/ notify_merchant()
Active ──archive──→ Archived
SoldOut ──restock──→ Active
When to Use
- When an entity has distinct modes of behavior (order, booking, product)
- When invalid transitions must be prevented (can't publish a deleted product)
- To communicate business rules clearly across teams