1.Valid transitions — and making the invalid ones impossible
The roadmap draws two machines: a payment goes `pending → processing → succeeded`, or `→ failed`; an order goes `draft → placed → processing → fulfilled`, or `→ cancelled`. What makes that a state machine rather than a list of words is the set of transitions that are **not** drawn — `succeeded → pending` is absent, and so is `fulfilled → draft`. A `status` field with `choices` enforces the legal *values* and says nothing about legal *moves*, so the transition table has to be written down and checked somewhere.
Key features
- `choices` constrains the value set, not the transitions between values
- `choices` is enforced by validation, not by the database — add a `CheckConstraint` for the values
- Write the edges as data: a dict of `state → set of reachable states`
- One `transition_to()` method; nothing else assigns to the status field
- Re-entering the current state is usually a duplicate request — succeed quietly
- A genuinely unreachable move is a 409 Conflict: the caller's view of the world is stale
The roadmap's two machines, written as a transition table
Where each rule can actually be enforced
- draft (start)
- → placed when customer confirms
- → cancelled when abandoned
- placed
- → processing when payment succeeded
- → cancelled when payment failed, or cancelled
- processing
- → fulfilled when shipped
- → cancelled when cancelled before dispatch
- fulfilled (end)
- cancelled (end)
Command
order.transition_to(Status.PLACED, actor=request.user) # the only way status changesA `status` field with `choices` constrains values, never moves — the machine is the set of *edges*, and the edges that are absent (`fulfilled → draft`) are the ones doing the work. Write the table as data on the model, add a `CheckConstraint` so the database enforces the value set that `choices` only validates in forms, and make `transition_to()` the single place the field is ever assigned so a grep is a real audit. Re-entering the current state is a duplicate request and should succeed quietly; a genuinely unreachable move is a 409.