Cruxly

Cruxly › System design › Design Exercises

Order Matching Engine

determinism first, and the fastest correct thing is one thread

Open the interactive lesson

Prompt: design the matching engine of an exchange. Accept buy and sell orders for an instrument, match them by price-time priority, publish the resulting trades and the order book. Latency matters in microseconds, and the book must be reproducible exactly from the input sequence.

40 minutes first. Almost every instinct from the other exercises in this track is wrong here.

Applying the four moves: Scope — accept orders, match by price-time priority, publish trades and book state, reproducible from input; clearing and settlement out. Estimate — latency in microseconds, which rules out network hops and disk on the hot path; throughput per instrument is high but each instrument is independent. Sketch — a single-threaded matching engine per instrument holding the book in memory, fed by a sequenced input log. Deep dive — the usual playbook (replicate, shard, cache) is wrong here: one thread per book is what makes matching deterministic, and determinism is what makes the input log a complete, replayable recovery mechanism.

Why the usual playbook is wrong here

Every other exercise in this track scales out: shard it, replicate it, make it eventually consistent. Here that instinct produces a worse system, and being able to say why is the whole exercise.

Scale first, so the claim is grounded. A busy instrument might see 100,000 orders/s at peak. That is a genuinely large number for one thread — and completely unremarkable for a modern CPU doing in-memory work with no I/O on the path. The data is small: an order book is a few hundred thousand resting orders, tens of megabytes.

Requirement 1: determinism. Replaying the same input sequence must reproduce the same book and the same trades, exactly. This is not a nice-to-have — it is how you recover from a crash, how you run a hot standby, how you settle a dispute, and in most jurisdictions a regulatory obligation. Concurrency is the enemy of determinism: two threads touching one book interleave differently on each run, so the same inputs stop producing the same outputs.

Requirement 2: fairness. Price-time priority means the earliest order at the best price trades first. "Earliest" has to mean something precise, and across several machines with skewed clocks it does not.

Both requirements resolve to the same answer: impose a total order on inputs, then apply them one at a time. Once you have done that, a second thread on the matching core buys you nothing — the order is already fixed — while costing determinism and adding lock contention to a path measured in microseconds.

:::key The counter-intuitive core of this problem Single-threaded is not a limitation you accept here; it is the design. The work is arranging everything else — parsing, risk checks, persistence, market-data publishing — so that the one thread that matches never waits for any of it. :::

TWO THREADS — same inputs, different outcomes order A, order B run 1: A then B run 2: B then A two different books — not replayable ONE THREAD — same inputs, same outcome, always order A, order B fixed order: A then B, always identical book on every run — replay works
Concurrency doesn't just risk a race here — it destroys the one property (exact replayability) that recovery, failover and regulation all depend on.

The architecture: sequencer, matcher, and everything off the hot path

gateways risk checks sequencer assigns seq no. total order matcher ONE thread in-memory book no I/O, no locks market data feed trade reports hot standby journal (async) all consume the same log
The sequencer creates the total order; the matcher applies it alone. Everything else is a downstream consumer of the identical sequence, so nothing downstream can ever slow the matching thread.

Gateways handle protocol parsing, authentication and pre-trade risk checks (does this account have the margin, is the price within limits). All of it happens before sequencing, because it is per-order work that parallelises freely — and because rejecting a bad order should never consume a sequence number.

The sequencer is the only coordination point: it stamps each accepted order with a monotonically increasing sequence number and appends it to a log. That number, not a wall clock, defines "earliest" — which sidesteps clock skew across gateways entirely. This is the one component that must not be scaled out, because being the single authority is its function.

The matcher consumes the log strictly in sequence order, holds the book in memory, and does nothing else. No database calls, no network calls, no allocation on the hot path if you can avoid it. One thread, one instrument (or a small group), no locks — because there is nothing to contend with.

Everything else consumes the same log downstream. The journal writes it durably for replay. Market data publishes book updates. The hot standby applies the identical sequence to its own copy of the book, so it is always one moment behind and ready to take over. None of these can block the matcher, because none of them is on its path.

The order book, and why it is an unusual data structure

A price-time priority book needs, on every incoming order: the best price on the opposite side (constantly), all orders at a price in arrival order (to match), and removal of an arbitrary order by ID (cancels are the majority of messages on a real venue).

book
├── bids: price → FIFO queue of resting orders   (descending)
└── asks: price → FIFO queue of resting orders   (ascending)

Why not a heap. A heap gives the best price in O(1) but cannot remove an arbitrary order by ID without a linear scan — and cancels dominate. Most real engines use price levels in a sorted structure (or, since instrument prices sit in a narrow band, a direct-indexed array of price levels, which makes "best price" a short walk and any price level an O(1) array index), plus a hash map from order ID to its node for O(1) cancel.

Matching is then mechanical:

on incoming BUY at price P, quantity Q:
    while Q > 0 and best_ask <= P:
        resting = front of queue at best_ask
        traded  = min(Q, resting.remaining)
        emit Trade(resting.id, incoming.id, best_ask, traded)
        Q -= traded;  resting.remaining -= traded
        if resting.remaining == 0: pop it from the queue
    if Q > 0: rest the remainder at price P   (or cancel it, if IOC)

Note the price: a trade executes at the resting order's price, not the incoming one. The resting order was there first and set the terms — that rule is the reason price-time priority is a fairness guarantee rather than just an ordering convention, and getting it backwards is a subtle, expensive bug.

Integer ticks, never floating point. Prices are integer multiples of a tick size, exactly as money is integer minor units in the ledger exercise. A book that accumulates floating-point error is one that eventually disagrees with itself about whether two prices are equal.

HEAP — best price O(1), cancel is a scan best price: O(1) cancel by ID: O(n) scan and cancels are the MAJORITY of messages PRICE-LEVEL ARRAY + ID MAP (chosen) array of price levels — direct index order_id -> queue node both best-price AND cancel are O(1)
The heap's classic strength — instant access to the extreme value — is exactly the wrong optimization target here, since the operation that actually dominates traffic is cancellation, not peeking.

Recovery, failover, and the latency work

Recovery is replay. The book is in memory and vanishes on crash. Restart, read the journal from the last snapshot, reapply every sequenced message in order, and you are in exactly the state you were in — because the matcher is deterministic. Periodic snapshots bound how much replay is needed. This is the payoff for the determinism requirement: recovery needs no reconciliation, only repetition.

Failover. The hot standby has been applying the same sequence all along. Promotion means confirming the primary is truly dead (a split brain with two matchers is catastrophic — both would emit conflicting trades) and pointing gateways at the standby. Fencing matters more here than almost anywhere: use a sequence-number-based fence so a resurrected old primary cannot append after the standby has taken over.

Where the microseconds actually go, worth naming because it separates people who have built one of these from people who have read about them:

The question to ask before any of that: what actually needs to be fast? The matching thread. Not the risk checks (already parallel, before sequencing), not the market data publish (downstream), not the journal (async). Most latency work fails by optimising a component that was never on the critical path.

:::warn What not to propose here Eventual consistency, sharding one instrument's book across machines, or a distributed lock around matching. All three break determinism or add coordination to a path whose entire budget is a few microseconds. Sharding across instruments is fine and standard — each instrument gets its own single-threaded matcher — but one book stays on one thread. :::

How to say it in the interview

"This is the one system where I do not want to distribute the hot path. Determinism and microsecond latency both point the same way — a single-threaded matching core fed by an ordered log, with everything else kept off that thread."

Checklist

Practice this interactively
Share this lessonWhatsAppLinkedInX

Related lessons