The order state machine
Every state an order can occupy, every legal transition — and why enumerating them kills a whole class of bugs.
Matching changes quantities while cancels and expiries can arrive nearby in the same sequence. The venue needs one model that decides which transitions remain possible.
A plausible simplification is to store independent booleans such as filled, cancelled, and expired, then let each handler update whichever flags seem relevant. The catch is that exactly one core writer applies one legal transition per sequenced event, and terminal states are absorbing: no later event can reopen an order or repeat its effects.
Distributed-systems bugs love undefined states: "the cancel arrived while the fill was in flight — now what?" The exchange's answer is to make the order's lifecycle an explicit state machine, and to let the matching engine alone perform transitions. Everyone else — gateways, risk, the trader — merely requests transitions and learns the verdict from execution reports.
stateDiagram-v2
[*] --> Pending: received by core
Pending --> Rejected: fails validation or risk
Pending --> Filled: fully matches on arrival
Pending --> PartiallyFilled: partly matches on arrival
Pending --> Live: no match, rests on book
Live --> PartiallyFilled: incoming order matches part
Live --> Filled: incoming order matches all
Live --> Canceled: cancel request honored
Live --> Expired: time in force runs out
PartiallyFilled --> Filled: remainder matches
PartiallyFilled --> Canceled: remainder canceled
PartiallyFilled --> Expired: remainder expires
Rejected --> [*]
Filled --> [*]
Canceled --> [*]
Expired --> [*]
The diagram resolves the classic race by construction. Alice sends a cancel at the same moment Bob's order is matching hers. Both requests enter the engine's single input stream (1.3); whichever the engine processes first wins, and the state machine makes the loser's outcome well-defined:
- Cancel first → order is
Canceled; Bob's order matches elsewhere or rests. - Match first → order is
Filled; the cancel arrives to find no live order and returns too late to cancel — an ordinary, expected response, not an error.
This is worth internalizing as a method: races don't get resolved by cleverness at the edges; they get resolved by total ordering at the core, plus a state machine with no undefined cells.
A cancel follows the event that consumes the final share. A flag-based model may mark the order both filled and cancelled and release inventory twice. An absorbing FILLED state turns “too late to cancel” into a normal, deterministic result.
Orders live in an explicit state machine with absorbing terminal states. Exactly one writer — the engine — performs transitions; all else requests. "Too late to cancel" is a normal answer, designed in, not an error bolted on.