The Order Lifecycle State Machine
An order isn't a single event, it's a sequence of states connected by rules about which transitions are even allowed. Skip the state machine and you get double-counted fills and phantom positions that only show up during a race condition.
Prerequisites: Order Book Mechanics
A live trading system sends a cancel for a resting order at the same moment the exchange sends back a fill for that same order — an ordinary race, not a bug on either end, just two messages crossing in transit. The system's order tracking code, written without an explicit model of what states an order can be in, processes the cancel first and marks the order dead, then processes the fill message and, finding no live order to attach it to, drops it. The position book never learns about a real fill for 3,000 shares. It shows up hours later as an unexplained cash discrepancy, the kind of bug that takes a day to trace because nothing crashed — the fill was simply thrown away by code that had no state to put it in.
The fix isn't a smarter special case for this one race — it's treating an order as what it actually is: a state machine, where an order occupies exactly one state at a time and only certain transitions between states are legal. Once that's explicit, "a fill arrived for an order I already marked cancelled" isn't a mystery to special-case, it's a transition the state machine either defines a rule for or explicitly rejects as invalid, and either way the fill can't just vanish.
The states, and why the shape matters
A typical order lifecycle has states along the lines of: New (created locally, not yet sent), Pending (sent, unacknowledged by the exchange), Acknowledged or Resting (confirmed live), Partially Filled, Filled, Pending Cancel, Cancelled, and Rejected. The value isn't the exact list — venues differ — it's that every one of these is mutually exclusive at any instant, and the transitions between them are the part worth designing deliberately: an order can move from Resting to Partially Filled to Filled, or from Resting to Cancelled, but it should never be legal to receive a fill for an order already in a terminal state like Cancelled without that being flagged as exactly the race condition described above, handled by a defined rule (e.g., a fill on a cancelled order still books the trade and reopens the position) rather than silently dropped.
Worked example: the same race, modeled correctly
Same scenario — a cancel and a fill in flight simultaneously for a 3,000-share resting order — but now with an explicit rule: any execution report received while an order is in Pending Cancel or Cancelled is still applied to the position, and the order's final state reflects whatever was actually filled versus cancelled. Trace it:
| Step | Event | State before | State after | Position impact |
|---|---|---|---|---|
| 1 | Order sent | New | Pending | none |
| 2 | Exchange acks | Pending | Resting | none |
| 3 | Cancel request sent | Resting | Pending Cancel | none |
| 4 | Fill arrives (crossed with the cancel) | Pending Cancel | Filled (with a note: cancel arrived late) | +3,000 shares booked |
| 5 | Cancel confirmation arrives for the remainder | Filled | Filled, cancel is a no-op on the already-filled quantity | no change |
Nothing is lost. Step 4 is exactly the message the naive system dropped; here it has a defined transition (fill overrides a cancel still in flight) instead of nowhere to go.
An order is a state machine, not a mutable record you patch in place. The valuable design work is enumerating every transition that can legally happen, including the awkward ones caused by messages crossing in flight, so no incoming message ever has nowhere to go.
Write down every state and every legal transition on paper before writing code, then explicitly ask "what happens if this message arrives while the order is in each of the other states" for every message type. Most of the bugs this concept prevents are the transitions nobody thought to ask about.
Doing it properly
Make illegal transitions loud, not silent — an execution report that doesn't match a defined transition from the order's current state should raise an alert, not get dropped or, worse, applied blindly to a state that didn't expect it. Use the same state machine in the backtest simulator and the live system where possible, since a simulator with a looser model of order state will systematically understate exactly the operational risk this concept exists to catch. See Building a Matching Engine Simulator for how the state machine connects to the matching logic that generates these events in the first place.
Keep the state machine per order, not per instrument or per strategy, and make sure amends (a request to change price or size on an already-live order) are modeled as their own transition rather than folded into cancel-and-replace logic implicitly, since many venues treat an amend as losing queue priority while others don't — a detail invisible unless the state machine has an explicit slot for it. It's also worth logging every transition with its triggering message, not just the current state, because reconstructing what actually happened during an incident depends on the full sequence of transitions, not a snapshot of where the order ended up. A system that only stores current state has already thrown away the evidence needed to debug the next race condition like the one that opened this page.
Related concepts
Practice in interviews
Further reading
- FIX Trading Community, FIX Protocol Specification (ExecType and OrdStatus)
- Harris, Trading and Exchanges