State Machine Lab

Lifecycles made explicit — and, more importantly, the transitions that must be impossible to express. The states are the easy part. The forbidden transitions are the design.

Adding one state to a lifecycle of five adds up to ten new ordered pairs, and the ones nobody enumerated are the ones that will happen — usually by a path that does not go through the code you wrote: an admin tool that assigns a status directly, an importer that predates the rules, a webhook that sets a state without reading the current one. So each machine below leads with what must be impossible rather than ending with it. A transition that is merely undesirable will occur; a transition that cannot be represented will not.

27 of 27 lifecycles
Read this first — 4 transitions that must be impossible
  • released → confirmedStock was returned to the pool and may already have been reserved by someone else. Confirming after release is how one unit gets promised twice, and it is exactly the race between a late payment webhook and the expiry job — which is why the release must be conditional and atomic rather than a read followed by a write.
  • confirmed → releasedThe stock is committed to a paid order. Releasing it means the customer paid for something the warehouse will give to someone else. A cancellation after confirmation is a different operation entirely — a return — with its own accounting, and collapsing the two is a common and expensive modelling error.
  • available → confirmedSkipping the reservation means the availability check and the commitment are two separate operations with a gap between them, which is precisely the read-check-write race the reservation exists to close. Any "fast path" that does this reintroduces double-selling under exactly the load where it hurts most.
  • oversold → confirmedAuto-confirming out of an oversold state destroys the evidence of how it happened. Oversold exists to be investigated; a transition that quietly resolves it turns a detectable bug into a permanent silent one (Swallowed Errors).

For each one, ask where it is prevented. If the answer is “the handler checks it”, then the importer, the back-office tool and the retry job each get to disagree. If the answer is “the type cannot express it” or “the constraint rejects it”, nothing gets to disagree.

Where this is taught
The intermediate state, made explicit
A stock reservation
availablereservedconfirmed ·released ·oversold ·
FromOnToGuardEffect
availablereserve(orderId, qty)reservedavailable >= qty, evaluated in the same statement that decrementsavailable -= qty; expiresAt = now + 15m
reservedorderPaidconfirmednow < expiresAt AND the reservation still belongs to this orderpromised += qty; reservation closed
reservedexpiry job OR customer cancelsreleasednot already confirmed — checked and written atomicallyavailable += qty
reservedreconciliation detects promised > availableoversoldonly reachable through a bug or a leakalert, and the order is held rather than cancelled
must be impossible
  • released → confirmedStock was returned to the pool and may already have been reserved by someone else. Confirming after release is how one unit gets promised twice, and it is exactly the race between a late payment webhook and the expiry job — which is why the release must be conditional and atomic rather than a read followed by a write.
  • confirmed → releasedThe stock is committed to a paid order. Releasing it means the customer paid for something the warehouse will give to someone else. A cancellation after confirmation is a different operation entirely — a return — with its own accounting, and collapsing the two is a common and expensive modelling error.
  • available → confirmedSkipping the reservation means the availability check and the commitment are two separate operations with a gap between them, which is precisely the read-check-write race the reservation exists to close. Any "fast path" that does this reintroduces double-selling under exactly the load where it hurts most.
  • oversold → confirmedAuto-confirming out of an oversold state destroys the evidence of how it happened. Oversold exists to be investigated; a transition that quietly resolves it turns a detectable bug into a permanent silent one (Swallowed Errors).

Note what oversold is doing here. It is not a state the design permits — it is a state the design *names*, so that reconciliation has somewhere to put a violation and a human has something to look at. A system with no name for its illegal states discovers them as support tickets (Making Illegal States Unrepresentable argues for removing states; this is the complementary move for the ones you cannot remove).