software-design/state-machines-why-an-order-can-t-be-delivered-before-it-s-paid.md
State machines: why an order can't be delivered before it's paid
An online order can be in the cart, paid, shipped, delivered or cancelled, but what stops the code from delivering an order nobody paid for? The answer is a state machine: one value for where the order is now, plus a short list of allowed moves.
Play with a tiny shop, try the moves that should be refused, and see why four true/false flags allow 16 combinations while five states allow exactly what you meant. Includes a JavaScript example that was run for real.

An online shop has an order. It starts in the cart, gets paid, gets shipped, and arrives. Now imagine the code lets someone click "Deliver" on an order that was never paid. Nothing crashes. The shop just quietly lies about what happened.
This kind of bug is common, and there is a plain way to prevent it: write down every state an order can be in, and every move that is allowed between them. The result is called a state machine.
The problem with a pile of true/false flags
A natural first attempt is one boolean (a value that is either true or false) per fact: isPaid, isShipped, isDelivered, isCancelled. Four flags feel simple. But each flag has two values, so four of them can be combined in 2 × 2 × 2 × 2 = 16 ways. Only a few of those 16 describe a real order. "Delivered but not paid" and "shipped and cancelled" are both possible to write down, and nothing in the code says they are wrong.
Every new flag doubles the count. The program ends up full of if checks that try to rule out the nonsense combinations, and sooner or later one is missed.
The idea: one state at a time
A state machine replaces the flags with one value that says where the order is right now. This value is the state. The order here has five states: cart, paid, shipped, delivered and cancelled. An order is in exactly one of them, so "delivered and cancelled at once" cannot even be expressed.
Then you list the allowed moves. Each move is triggered by an event (something that happens, such as "pay") and leads from one state to another. A move is called a transition. Anything not on the list is not allowed.
Try it: play the shop
Press the event buttons below. Some moves work. Others are refused, and that refusal is the whole point.
The same thing as a table
Here are the allowed transitions written as a table. Read a row as "from this state, this event leads here".
| From | Event | To |
|---|---|---|
| cart | pay | paid |
| cart | cancel | cancelled |
| paid | ship | shipped |
| paid | cancel | cancelled |
| shipped | deliver | delivered |
Five rows are the whole rulebook. delivered and cancelled have no rows at all, so they are final states: once an order gets there, nothing else can happen to it. Whether a shipped order can be cancelled is a business decision. Here the answer is no, and the table makes that decision visible instead of hiding it inside if statements.
The code is short
In JavaScript the table fits in an object (a collection of named values). Each state is a key, and its value lists the events that state accepts.
const transitions = {
cart: { pay: 'paid', cancel: 'cancelled' },
paid: { ship: 'shipped', cancel: 'cancelled' },
shipped: { deliver: 'delivered' },
delivered: {},
cancelled: {}
};
function send(state, event) {
const next = transitions[state][event];
if (next === undefined) {
throw new Error("Cannot " + event + " an order that is " + state);
}
return next;
}
let order = "cart";
order = send(order, "pay");
console.log(order); // paid
order = send(order, "ship");
console.log(order); // shipped
send(order, "cancel"); // throws an error
Running this in Node.js prints paid, then shipped, and the last line stops the program with the message Cannot cancel an order that is shipped. (When a function raises an error with throw, the program stops there unless some other code catches it.) The function never needs to know what a "good" order looks like. It only looks the move up in the table.
Where you will meet this
State machines are everywhere once you look. A traffic light cycles through red, green and amber. A video player is stopped, playing or paused. A login form is empty, submitting, failed or signed in. A network connection is closed, connecting or open. In each case the real bugs hide in the gaps: pressing "play" twice, or submitting while already submitting.
What to take from this
When you catch yourself adding a third or fourth true/false flag to describe one thing, stop and ask: is this really one thing that is in one of several states? If so, list the states, list the allowed moves, and let anything else be refused loudly. On paper this takes five minutes. It can save hours of hunting for an order that is somehow both cancelled and delivered.