all_lessons / system_design / cases / C39 C39 / C44

Match buy and sell orders

The matching engine is the exchange's kernel: a tiny, single-threaded state machine whose only job is to decide — for a stream of orders — exactly which trades happen, in exactly what order, every single time you replay the tape. Correctness is the product. Speed is a constraint.

Satellite of C37 (exchange anchor) Owns: matching kernel Price-time priority Deterministic replay
Where this sits in the exchange arc
This is one of three deliberately linked cases. C37 is the system overview — gateways, sequencer, order book, market-data fan-out, the durability story — and is the anchor you should read first. C38 owns the microsecond latency budget (kernel bypass, busy-poll, NUMA pinning). This page, C39, owns the matching kernel itself: the price-time priority rule, the data structure that makes "best price" O(1), and why the whole thing must replay deterministically. We will not re-derive C37's architecture — we link to it and go deep on the kernel.

The hinge: matching is a deterministic state machine, not a database query

First principle
A matching engine is a pure function: match(state, command) → (state', trades). Given the same starting book and the same ordered sequence of commands, it must produce byte-identical fills — on the primary, on the hot standby, on the auditor's machine three years later. That is not a nice-to-have; it is the definition of a fair market. Every design decision below is downstream of this one requirement: the output must be a deterministic function of an agreed input order.

Notice what this rules out. The engine cannot consult wall-clock time mid-match (two replays would diverge), cannot read a shared cache (stale reads differ), cannot run the matching loop on multiple threads racing for the same price level (interleavings differ), and cannot let floating-point summation reorder. The forcing constraint is reproducibility under replay, and it pushes the design toward something that feels almost retro: a single thread, all state in memory, fed by one totally-ordered input log. This is DDIA Ch. 7's deepest insight made literal — the strongest isolation level, serializability, is defined as "as if executed serially, one at a time," and the matching engine simply is a single-threaded serial executor. There is no isolation anomaly because there is no concurrency.

1. Clarify the contract — and what it is NOT

Treat the prompt as a product contract before a box diagram. The kernel must:

And explicitly what it need not do, because these belong to C37's surrounding system, not the kernel: it does not persist to a database on the hot path (durability comes from the sequenced input log, replayed into a hot standby — see below), does not do auth, risk-limit, or balance checks (the gateway does pre-trade risk before the sequencer), does not handle multi-symbol sharding policy (C37: one symbol → one kernel instance), and does not own the wire latency budget (C38). Keeping the kernel this small is what lets it stay a few thousand lines of branch-predictable, allocation-free code.

2. Put numbers on the data structure — array of price levels vs tree map

The single most consequential kernel decision is how to store price levels. The matching loop does one thing constantly: find the best (top-of-book) price, repeatedly. So the question is "what is the cost of `best_bid()` / `best_ask()`, and of insert/cancel?" Two candidates:

Structurebest-priceinsert at levelcancelmemory
Sorted tree map (RB-tree / skip list keyed by price)O(log L)O(log L)O(log L) node removalO(Lactive) — only populated levels
Dense array of price levels (price index → level)O(1) amortised (track a moving best pointer)O(1)O(1) tombstoneO(Lmax) — all ticks pre-allocated

Now do the arithmetic that decides it. Take an instrument with a bounded, regulated tick grid: price range $0.01 to $1000 at a $0.01 tick. The number of representable price levels is

Lmax = (1000 − 0.01) / 0.01 ≈ 100,000 levels

Pre-allocate an array of 100,000 slots, each a small struct (head/tail pointer into a FIFO queue + aggregate qty), say ~32 bytes. That is 100,000 · 32 = 3.2 MB per side, ~6.4 MB for the full book — trivially L1/L2-adjacent, cache-friendly, and never touches the allocator after startup. In exchange for that fixed memory you get O(1) access to any price level by direct indexing and an O(1) best-price read by advancing a single "best level" cursor. Compare the tree map: it only stores the handful of active levels (maybe a few hundred), so its memory is smaller, but every best-price read pays O(log L) ≈ log2(100,000) ≈ 17 pointer-chasing, cache-missing comparisons — and you do this on the hottest line of the hottest loop in the building.

Price levels (1c tick, $0.01–$1000)
~100,000
Dense array, full book
~6.4 MB
best-price: array vs tree
O(1) vs ~17 hops
Orders that never fill (cancelled)
~90%
The condition that flips the answer
The dense array wins only when the tick grid is bounded and not sparse — equities and futures, where regulated tick sizes cap Lmax in the hundreds of thousands. For instruments with a huge or unbounded price range relative to the tick (some crypto pairs span many orders of magnitude, or use floating tick sizes), the array becomes mostly empty and the memory blows up — there the tree map's O(Lactive) memory is the right call, and you pay the log L. Production equity engines almost universally pick the array; this is the textbook "tempting shortcut" only when you forget to check that the price range is actually bounded.

3. The cancel-rate fact that drives the queue design

One operational number dominates the data-structure choice for within a price level: in modern markets, roughly 90% of submitted orders are cancelled before they ever fill. The order-to-trade ratio at high-frequency venues runs well above 10:1. So cancel is not a rare path — it is the dominant path. That changes the math entirely.

If you model each price level's FIFO as a doubly-linked list and store, in an order-ID → node hash map, a direct pointer to each resting order, then both insert and cancel are O(1). But a true node removal on cancel costs you the pointer surgery plus map deletion plus potential allocator churn — and you pay it on 90% of orders. The cheaper alternative is a tombstone: on cancel, flip a `cancelled` flag on the node and leave it in the queue; the matching loop skips tombstoned nodes lazily when it walks the level, and a background compaction reclaims them. Tombstoning makes cancel a single branch-free store, at the cost of queues that bloat with dead nodes between compactions. Given a 90% cancel rate, the tombstone's "do almost nothing on the hot cancel path, clean up in bulk later" is usually the right trade — it is the same LSM-tree intuition from DDIA Ch. 3 (cheap deletes via markers, amortised compaction) applied to an in-memory queue.

4. Data model & API

Keep the surface minimal; the kernel speaks in commands, not internal layout.

CommandEffect
new_order(id, side, type, price, qty)Match against the opposite book; rest the residual (limit) or reject the residual (market/IOC).
cancel_order(id)Tombstone the resting order found via the order-ID map.
replace_order(id, price', qty')Cancel + re-add. A price change loses time priority (goes to the back of the new level) — a fairness rule, not an implementation detail.
snapshot_book()Deterministic dump of resting orders for recovery / audit.

The core records: a price level (FIFO queue head/tail + aggregate qty), an order (id, side, price, remaining qty, queue node), an order-ID map (id → node, for O(1) cancel), a trade (maker id, taker id, price, qty, seq), and the sequence number stamped by the sequencer on every command.

5. Linearized design — follow one aggressive order

  1. Sequence first. Every accepted command passes through the C37 sequencer, which stamps a monotonic sequence number. This is the total order; the kernel only ever sees commands in this order. (DDIA Ch. 9: total order broadcast.)
  2. Read the opposite top-of-book. For a buy, look at the best ask via the O(1) best-price cursor.
  3. Cross test. If the buy's limit price ≥ best ask, it is marketable — match. Otherwise rest it.
  4. Walk FIFO within the level. Fill against the oldest resting order at that price first, then the next, emitting a trade per fill, decrementing both quantities. Skip tombstoned nodes.
  5. Sweep levels. If the order still has quantity and the next price level is still marketable, advance the best cursor and repeat (this is how a large/market order "walks the book").
  6. Rest the residual. Any unfilled limit quantity is inserted at the back of its own price level (FIFO tail); a market/IOC residual is cancelled.
  7. Append outputs to the log. Commands in, trades out — both appended to the sequenced log that feeds the hot standby and the audit trail.
PRICE-TIME PRIORITY — an aggressive BUY 250 @ limit 100.02 walks the ask side ASK book (sellers, ascending) incoming: BUY 250 @ 100.02 price FIFO queue (oldest → newest) ───── ────────────────────────────── 100.00 [A:100][B: 80] ← best ask, fill these first (better price) 100.01 [C:120] 100.02 [D: 60] ← still marketable (100.02 ≤ limit 100.02) 100.03 [E:200] ← NOT marketable, untouched step 1: best ask = 100.00. FIFO: fill A(100) then B(80) → 180 done, 70 left step 2: advance to 100.01. fill C: take 70 of 120 → C partial, 50 rests; buy done step 3: residual 0 → nothing to rest. TRADES (in sequence): A↔buy 100@100.00 · B↔buy 80@100.00 · C↔buy 70@100.01 book after: 100.00 empty · 100.01 [C:50] · 100.02 [D:60] · 100.03 [E:200] Better price ALWAYS fills before worse price. Same price → earliest order (A before B). Identical input sequence ⇒ identical trades ⇒ deterministic replay.

6. Deep dives

(a) Price-time priority and why the structure follows the rule

The rule is two-level: price priority (a better price always trades first) then time priority (at equal price, the order that arrived earlier trades first). The data structure is just this rule made physical. Price priority is why levels are sorted and why best-price must be cheap — you always touch the top of the book first. Time priority is why each level is a FIFO queue and why every command carries a monotonic sequence number: "earlier" is defined by sequence position, not wall clock. Get those two structures right and the matching loop is almost trivial — "while marketable, fill the front of the best opposite level." Note the fairness corollary mentioned above: changing an order's price moves it to the back of the new level, because otherwise you could jump the queue for free.

(b) Deterministic replay = state-machine replication

Durability here does not come from writing the book to a database on each trade — that would put a disk round-trip in the microsecond hot path. Instead: persist only the sequenced input log, and recover the book by replaying it through the same deterministic kernel. This is exactly DDIA Ch. 9's state-machine replication and Ch. 11's event sourcing: the log of commands is the source of truth; the order book is a derived view, a fold over the log. A hot standby runs the identical kernel against the identical sequenced stream and stays bit-for-bit in lockstep, ready to take over with zero divergence. The auditor can reconstruct any trade from three years ago by replaying the tape. None of this works unless the kernel is a pure deterministic function — which is why the "no wall clock, no shared cache, single thread, no FP reordering" rules from the hinge are non-negotiable. The sequencer providing the total order (Ch. 9, total order broadcast — itself a consensus problem, foundation lesson 10) is the keystone: one agreed input order makes one reproducible output.

(c) Tombstone vs node removal under 90% cancels; market vs limit

Covered numerically in §3: with ~90% of orders cancelled, the cancel path is the hot path, so a tombstone (flag-and-skip, bulk-compact later) usually beats true node removal (pointer surgery + map delete + allocator churn) on the hot path, at the cost of dead-node bloat between compactions. Choose true removal only if memory pressure or queue-walk overhead from tombstones measurably dominates — i.e. when cancels are rare, which is the opposite of these markets.

On market vs limit: a market order has no price bound, so it walks the book sweeping every level until filled — which means in a thin book it can fill at a far worse price than the top, the slippage risk. Limit-only venues never slip past the limit but can leave orders unfilled. Retail-facing venues offer market orders for simplicity and bolt on protection — price collars / banding that reject or convert a market order if it would walk too far from the reference price. The kernel stays deterministic either way; the collar is just another deterministic check on the sequenced command.

7. Trade-offs

ChoiceBuysCostsChoose when
Array of price levels vs tree mapO(1) best-price & insert; cache-friendly, allocation-free~6.4 MB fixed even for sparse books; needs a bounded tick gridBounded-tick instruments (equities, futures) — the common case
Tombstone cancel vs node removalBranch-free O(1) cancel on the dominant pathDead-node bloat; needs background compactionHigh cancel rate (~90% order-to-trade) — i.e. always, in modern markets
Market orders vs limit-onlyUser simplicity; guaranteed fillSlippage; needs collars/bandingRetail-facing venues
In-memory book + log replay vs database bookMicrosecond matching; deterministic recoveryDurability lives in the log, not the book; needs replication disciplineAny real matching engine

The sharpest of these is the last: putting the book only in memory feels reckless until you internalise that the sequenced log is the durable truth and the book is a replayable derivation (DDIA Ch. 11). A database-backed book would be durable but would add a disk write to a path measured in nanoseconds — and would still not give you deterministic replay for audit. The in-memory-plus-log design gets both microsecond latency and stronger auditability, precisely because it inverts the usual "the database is the source of truth" instinct.

8. Failure modes

FailureMitigation
Replay diverges (non-determinism)Ban wall-clock reads, shared caches, multi-threaded matching, FP reordering in the kernel; make it a pure function of sequenced input.
Cancel races a fill for the same orderRoute both through the one sequencer; the lower sequence number wins, deterministically (see Q&A).
Bad tick / out-of-grid priceValidate tick increment at the gateway before sequencing; the kernel assumes clean input.
Book corruption / standby driftPeriodic deterministic snapshot + checksum; rebuild by replaying the log from the last good snapshot.
Tombstone bloat starves cacheBound queue dead-ratio; trigger compaction at a threshold.

9. Interview Q&A

  1. A cancel and a fill for the same order race — which wins, and how do you make it deterministic? (senior answer) They don't actually "race," because both events pass through the one sequencer first and are stamped with sequence numbers. The kernel processes commands strictly in sequence order, single-threaded. So the winner is simply whichever has the lower sequence number: if the cancel was sequenced before the matching event that would fill the order, the order is already tombstoned and cannot fill; if the fill's command sequenced first, the order fills (possibly fully) and the later cancel finds nothing or only the residual. There is no nondeterminism because there is no concurrency at the matching step — the total order is the tiebreak. This is DDIA Ch. 9's total-order-broadcast turning "who got there first?" into "what is the smaller sequence number?", which every replica answers identically.
  2. Why an array of price levels instead of a balanced tree? (senior answer) For a bounded tick grid ($0.01–$1000 at 1c ≈ 100k levels, ~6.4 MB), the array gives O(1) best-price and O(1) insert/cancel with a cache-friendly, allocation-free layout, versus the tree's ~17-comparison, cache-missing O(log L) on the hottest loop. The tree only wins when the price range is sparse/unbounded relative to the tick, where the array's fixed memory blows up — so check that assumption before defaulting to the array.
  3. How is the book durable if it lives only in memory? (senior answer) Durability comes from the sequenced input log, not the book. The book is a deterministic fold over that log (event sourcing, DDIA Ch. 11); recover by replaying the log into the identical kernel. A hot standby replays the same stream live and stays bit-identical. This keeps the hot path off disk while making recovery and audit stronger than a DB-backed book.
  4. Why FIFO within a price level, and what does a price-change replace do to it? (senior answer) FIFO encodes time priority — at equal price, the earlier order trades first, where "earlier" means lower sequence number. A replace that changes price loses time priority and goes to the back of the new level, otherwise traders could repeatedly re-price to jump the queue for free. Quantity-only decreases can sometimes keep priority depending on venue rules.
  5. Why single-threaded? Isn't that leaving performance on the table? (senior answer) Single-threaded matching is what makes the engine a serializable state machine (DDIA Ch. 7 — serializability is literally "as if run serially"), which is what makes deterministic replay and lockstep standbys possible. You buy correctness and auditability. You recover the lost parallelism by sharding by symbol (C37): one kernel per symbol, many kernels across cores — each independently deterministic.
  6. How do you handle the ~90% cancel rate cheaply? (senior answer) Tombstone on cancel — flip a flag, skip lazily during the level walk, compact in the background — so the dominant path is a single branch-free store rather than pointer surgery plus allocator churn. Keep an order-ID → node map for O(1) lookup. Bound the dead-node ratio so tombstones don't starve cache.
  7. What protects a market order from disastrous slippage? (senior answer) Price collars / banding: a deterministic pre-match check that rejects or halts a market order if it would walk beyond a band around the reference price. It keeps the kernel deterministic (it's just another check on the sequenced command) while protecting retail users from sweeping a thin book.

Related lessons

Read C37 first — it is the exchange overview anchor (sequencer, fan-out, durability, symbol sharding) that this kernel plugs into. C38 owns the microsecond latency budget that this single-threaded kernel must hit. Foundation lesson 10 (consensus & coordination) is where the sequencer's total order ultimately comes from, and 09 (consistency & CAP) frames why a strongly-ordered single writer is the right consistency model for a fair market.

C37 exchange overview C38 latency budget 10 · consensus 09 · consistency 12 · transactions/idempotency
Takeaway
The matching kernel is a deterministic single-threaded state machine: match(state, command) → (state', trades), fed by one totally-ordered input log. Price-time priority is the rule; a dense array of price levels (~6.4 MB for a bounded tick grid) makes best-price O(1); FIFO queues with tombstone cancels handle the ~90% cancel rate cheaply. Durability and lockstep standbys come from replaying the sequenced log (state-machine replication / event sourcing), and the "cancel-vs-fill race" dissolves because both events carry sequence numbers — the lower one wins, identically, on every replica. Get the order right and the rest is bookkeeping.