Software Architect · Module 09
Most of the hard bugs don't come from algorithms. They come from implicit states and the transitions between them.
State machine · lifecycle · concurrency · consistency
If an object has a lifecycle, design it as a state machine — not as a pile of boolean flags.
State has to be explicit
A traffic light is understandable because it has a limited set of states and permitted transitions. If all lamps lit at once, drivers would be arguing in the intersection.
Order, payment, delivery, subscription, ticket, deployment — these are entities with a lifecycle. They have states and transitions: draft, pending_payment, paid, cancelled, refunded. The architectural work is to define the allowed transitions and the owner of each one.
When you get flags like isPaid, isCancelled, isRefunded, isArchived instead of a state machine, the system quickly reaches impossible combinations: paid and cancelled at the same time, delivered without payment, refund without capture.
Concurrency is part of the model
If two people edit one document at the same time with no rules, the winner isn't whoever's right — it's whoever saved last.
In a real system several processes can change the same state: the user, a worker, a webhook, an admin, a retry. You need optimistic locking, a version field, transactions, unique constraints, or compare-and-swap operations.
The architect has to ask: who is allowed to change this state, how are races resolved, which operations are idempotent, and what counts as a final state.
Good state can be explained with a diagram. Bad state has to be explained with a list of exceptions.
Example: payment lifecycle
A bank operation goes through stages. You can't first return the money and then decide whether it was charged.
A payment can move from created to authorized, then to captured or voided. A refund is only possible after captured. The provider's webhook doesn't write arbitrary fields — it triggers a transition that checks the current state and version.
That approach makes edge cases part of the model, not a pile of if statements scattered across the code.
Anti-example: flags instead of a lifecycle
A form with ten checkboxes feels flexible — until two of them contradict each other.
The order has paid_at, cancelled_at, failed_at, refunded_at, completed_at, but no single clear status. Different parts of the code interpret the combinations differently. Support sees one thing, accounting sees another, the API sees a third.
That isn't flexibility. It's an implicit state machine with no rules.
- Which states does this entity have? - Which transitions are forbidden? - Who owns the transition: the user, the system, or an external provider? - How do we protect against retries and races?