Design a stock exchange
An exchange is a strange system: modest data, enormous message rates, and latency measured in microseconds. But the hardest requirement isn't speed. It's fairness you can prove: every order processed in the order it arrived, every trade reproducible from the record, months later, for a regulator.
1. The prompt
"Design a stock exchange: traders submit buy and sell orders, the exchange matches them and reports trades, and everyone sees the current prices."
What makes it hard:
- Fairness must be exact. Two orders at the same price are filled in arrival order; there is no "roughly".
- Latency matters at the microsecond scale, and the slowest tail matters more than the average.
- Nothing can be lost or double-counted, even across a server failure in the middle of a trade.
2. Frame the problem
How many instruments, and how busy?
About 10,000 symbols; peaks of around 500,000 order messages a second across all of them, with the busiest symbol at perhaps 50,000 a second at the open.
Load is spread over symbols, and one symbol's book is small: we can shard by symbol and keep each book in one thread's memory.
Which order types?
Limit orders, market orders, cancel and cancel-replace; time-in-force day or immediate-or-cancel.
The matching rules stay simple: price-time priority. Exotic types are follow-ups.
Who connects, and how?
Brokers and trading firms, many in our data centre (colocation), over a binary or FIX protocol on long-lived TCP connections.
Not REST: every request/response round trip and text parsing costs microseconds we don't have.
What latency do clients expect?
Order in to acknowledgement out in well under a millisecond inside the data centre, consistently.
The tail (p99.9) is the target: no garbage-collection pauses, no locks, no disk waits on the hot path.
What market data do we publish?
Best bid and offer, depth by price level, and every trade, to about 1,000 subscribers.
One stream to many readers at once: multicast, with sequence numbers so gaps are detectable.
Risk checks?
Yes, before matching: per-account limits, price bands against fat-finger errors, and a kill switch.
They're on the hot path, so they must be fast and in memory.
Clearing and settlement?
Out of scope; we hand trades to a clearing house.
Keeps us on matching. The handoff is a durable trade feed.
Audit?
Every order, cancel and trade kept for years, with accurate timestamps; we must be able to reproduce any day exactly.
That points to an event log as the source of truth, and a deterministic engine that can replay it.
3. What it must do
- A trader can place and cancel orders and gets an acknowledgement for each.
- The exchange matches orders by price-time priority and reports every fill to both sides.
- Everyone receives market data: the order book's top levels and every trade, in order.
Not now: clearing and settlement (a separate system), opening and closing auctions (a follow-up), routing to other exchanges, user accounts and onboarding.
4. How well it must do it
| Target | Why this number |
|---|---|
| Fair and deterministic: orders for a symbol processed strictly in arrival order; replaying the log reproduces every trade exactly consistency · rules | Fairness is the product; replay is how you prove it. |
| Latency: order to acknowledgement under 100 µs at p99 inside the data centre, with a tight tail speed | Participants compare exchanges on it; a slow tail is a fairness problem too. |
| No lost orders: an acknowledged order or trade survives any single machine failure data safety | "Your order was accepted" must stay true. |
| Failover in seconds, without duplicate or missing trades surviving failures | Markets can't wait minutes; a double fill is worse than a short halt. |
| Market data: every subscriber sees the same events in the same order, and can detect and repair gaps consistency | Traders act on it; missing one update means a wrong book. |
5. Numbers that change the design
| Quantity | Working | → so we… |
|---|---|---|
| Orders, busiest symbol | 50,000 a second; a match is a few hundred nanoseconds of in-memory work | One core can handle one symbol easily → so each symbol's book lives on one thread, with no locks |
| Orders, total | 500,000 a second over 10,000 symbols | Too much for one core in total → so we shard symbols across engine processes |
| Event log | 500,000 events × ~100 bytes ≈ 50 MB/s; a trading day ≈ 1–2 TB | Sequential appends are cheap → so every event is journaled and replicated before it's acknowledged |
| Book size | A few thousand resting orders per symbol | Kilobytes to megabytes → so everything fits in memory; disks are only for the log |
| Network round trip in one data centre | ~10–50 µs | Replicating to a standby before acking costs one round trip → so the standby sits in the same data centre; a second site gets the log asynchronously |
Skipped on purpose: storage for history (cheap), and user counts (irrelevant: the load is orders, not users). All figures are illustrative.
6. The nouns
- Order: who, which symbol, buy or sell, price, quantity, type, and the sequence number the exchange gave it.
- Order book per symbol: resting buy orders (bids) and sell orders (asks), by price level, oldest first within a level.
- Trade (a fill): price, quantity, the two orders, a trade id.
- Event: anything that changes a book, in the sequenced log.
order order_id, account, symbol, side, price_ticks (integer), qty, type, tif, seq book symbol → bids: price → FIFO of orders · asks: price → FIFO of orders trade trade_id, symbol, price_ticks, qty, buy_order_id, sell_order_id, seq event seq (per shard, gap-free), time, kind (new | cancel | fill | …), payload (append-only)
Prices are integers in ticks (the minimum price step), never floating point: two orders at "the same price" must compare equal.
7. The contract
Order entry (binary over TCP, or FIX)
NewOrder {client_order_id, symbol, side, price, qty, type, tif}
Cancel {client_order_id}
← Ack {client_order_id, exchange_order_id, seq}
← Fill {exchange_order_id, price, qty, trade_id, leaves_qty}
← Reject {client_order_id, reason}
Market data (UDP multicast, one stream per channel of symbols)
← AddOrder / Cancel / Trade / … each with a channel sequence number
Retransmission: TCP request for missed sequence numbers
Snapshot: the full book every few seconds, for late joiners
The account comes from the authenticated session, never from the message. client_order_id is unique per session, so a resent order is recognised, not doubled.
Data flow: gateway (session, risk checks) → sequencer (assigns the order) → replicated log → matching engine (deterministic) → acknowledgements and fills to gateways, events to market data → trade feed to clearing.
8. First working version
Gateways, one matching engine holding every book in memory, an execution report back to the trader, and a market data publisher.
8.1 A limit order that rests
A buy at 100.02 when the best ask is 100.05 can't trade, so it joins the bid side at its price level, behind orders already there.
8.2 An order that matches
A sell for 300 at 100.00 is "aggressive": it crosses the best bid. It fills against the best price first, and within a price, the oldest order first. Each fill produces execution reports for both sides and one trade on the market data feed.
8.3 Cancel
The engine finds the order through a map from order id to its place in the book and unlinks it in O(1). If it already filled, the cancel is rejected: whatever reached the engine first wins.
9. Make it hold up
9.1 The order book latency
| Approach | Why | |
|---|---|---|
| ✗ Bad | A database table of orders, queried for the best price on every order | Milliseconds per match, and locks between concurrent orders. |
| ~ Good | In memory: two sorted maps (price → FIFO list) and a map from order id to list node | O(log P) to find a level, O(1) to add, fill and cancel at a level. Fine for most symbols. |
| ✓ Great | Price levels in an array indexed by tick around the current price, plus the sorted map for far-away prices; intrusive linked lists and preallocated order objects | The touch (best prices) is O(1) with no allocation, so no garbage-collection pauses and very few cache misses. |
Matching an incoming order walks the opposite side from the best price, filling the oldest orders at each level until the order is filled or the price no longer crosses. Then any remainder rests (a limit order) or is cancelled (market or immediate-or-cancel).
9.2 One sequence, one thread fair and deterministic
| Approach | Why | |
|---|---|---|
| ✗ Bad | Many threads matching one book with locks | Lock contention adds latency, and the order of fills depends on thread scheduling: not reproducible, arguably not fair. |
| ~ Good | A queue per symbol, one thread per book | Deterministic per symbol, but the order between gateways depends on queue timing that isn't recorded. |
| ✓ Great | A sequencer stamps every inbound message with a gap-free sequence number and appends it to the log; the matching engine is a deterministic state machine that applies the log in order on a single thread | Arrival order is decided once and recorded. Same log, same trades, every time: on the standby, in tests, in an audit. This is the design LMAX described publicly. |
Determinism has rules: no reading the clock inside the engine (time comes in the event), no random numbers, no iteration over unordered hash maps, no floating point for prices.
9.3 Surviving the loss of an engine no lost orders · failover
| Approach | Why | |
|---|---|---|
| ✗ Bad | Snapshot the book to disk every minute | A crash loses up to a minute of acknowledged orders. |
| ~ Good | Write each event to local disk before acknowledging | Survives a process crash, not the machine. |
| ✓ Great | Replicate each sequenced event to hot standbys before acknowledging; standbys apply the same log and hold the same book; on failure one is promoted with a fencing token (Module 21) so the old primary can't keep trading | Failover in seconds with the exact same state; no double fills, because only one fenced leader writes to the log. |
Periodic snapshots still help: a restarting replica loads the latest snapshot and replays only the log after it. A second data centre receives the log asynchronously for disaster recovery, accepting a small loss window there.
9.4 Market data to a thousand subscribers market data
- UDP multicast: one packet reaches every subscriber at once, so the slowest reader can't slow the others, and everyone gets it at the same time.
- Sequence numbers per channel: a subscriber that sees 101 after 99 knows it missed 100.
- Gap recovery: request missing messages from a retransmission server over TCP, or rebuild from a periodic snapshot plus the updates after it.
- Incremental updates (add, change, delete at a price level) instead of the whole book each time.
- Two identical feeds on separate networks (A/B arbitration): subscribers take whichever copy of each packet arrives first.
9.5 Microseconds latency
- No allocation on the hot path: preallocated objects and ring buffers; in garbage-collected languages, no garbage, so no pauses.
- One pinned core per engine thread, busy-polling instead of sleeping; no context switches.
- Kernel-bypass networking for gateways and market data; binary messages with fixed layouts, no parsing of text.
- Measure the tail (p99.9 and maximum), on every component, with synchronised clocks.
9.6 Risk checks before matching rules
The gateway checks each order against the account's limits (open exposure, order size) and a price band around the last trade, using state kept in memory and updated from the engine's fills. A kill switch blocks all new orders from an account instantly. These checks run before the sequencer, so a rejected order never enters the book; the engine stays simple and fast.
9.7 The whole design after the deep dives
10. Interviewer follow-ups
The market opens and one symbol gets 20× its normal traffic.
One thread per symbol still copes: matching is sub-microsecond. If a symbol truly outgrows a core, it gets a dedicated engine process. Many exchanges also open with an auction: orders collect for a period, then one price is computed that matches the most volume, which spreads the opening burst.
How do you add more symbols or more traffic overall?
More engine shards, each owning a set of symbols with its own sequencer and log. Gateways route by symbol. There are no cross-symbol transactions, which is what makes this sharding clean.
The price moves 10% in a second.
Volatility controls: price bands that reject orders far from the reference price, and trading pauses (circuit breakers) that halt a symbol briefly, as US equity markets do under their limit up-limit down rules.
Two gateways receive orders at the same nanosecond. Which is first?
Whichever the sequencer stamps first; that's the definition. To make it fair, keep paths from gateways to the sequencer equal in length and latency, and record arrival timestamps from synchronised clocks for audit.
A regulator asks why order X didn't fill at 10:31:02.
Replay the log up to that sequence number in a test engine and inspect the book at that moment. Determinism makes this an exact answer, not a reconstruction.
The primary engine is slow, not dead.
Health checks on latency, not just liveness; a stuck primary is fenced and a standby promoted. Fencing matters most here: a slow primary that wakes up must not write.
A subscriber loses packets constantly.
That's its problem to fix (network, buffers), but it can always recover: retransmission for small gaps, snapshots for large ones. The feed never slows down for it.
How would a crypto exchange differ?
Usually internet clients over WebSockets instead of colocation, 24/7 trading so no daily restart, custody of assets in wallets, and often less strict latency. The matching engine core is the same.
Can we run the engine in the cloud?
The logic, yes. The latency and fairness targets are hard with shared, virtualised networks; that's why the biggest exchanges run in their own data centres, though some cloud deployments exist.
11. When things break
| Failure | What users see | How we detect it | What limits the damage |
|---|---|---|---|
| Engine primary dies | A pause of seconds for its symbols | Heartbeats from the primary | Hot standby with the same book; fencing |
| Sequencer or log replica lost | Nothing, or a brief pause | Replication acks missing | Acknowledge only after replication; replace the replica |
| Gateway down | Its sessions disconnect | Session heartbeats | Clients reconnect to another gateway; client order ids prevent duplicates |
| Market data packet loss | A subscriber's book briefly wrong | Sequence gaps | Retransmission, snapshots, A/B feeds |
| Runaway algorithm | Floods of bad orders from one firm | Risk limits, order-rate limits | Price bands, kill switch per account |
| Whole data centre lost | Trading halts | Site monitoring | Asynchronous log copy in a second site; resume after reconciliation |
12. Wrap-up and the clock
Trade-offs we made:
- One thread per book instead of parallel matching: simpler, faster and deterministic.
- Acknowledge after replication within one data centre; the second site accepts a small loss window.
- Shard by symbol and forbid cross-symbol transactions.
- Multicast market data; subscribers repair their own gaps.
- Risk checks at the gateway, keeping the engine minimal.
What we'd watch: order-to-ack latency (p50, p99.9, max) per shard · sequencer and replication lag · rejected orders by reason · market data gaps and retransmission requests · failover drills passed.
At 10×: more symbol shards and gateways; the per-symbol design doesn't change, because no single book gets 10× bigger work than a core can do.
A 60-minute interview
| 0–5 | Frame: symbols, order rates, latency, audit |
| 5–10 | Must do, how well, numbers |
| 10–15 | Nouns, protocol, data flow |
| 15–25 | First version: rest, match, cancel |
| 25–50 | Deep dives: book (9.1), sequencer (9.2), failover (9.3) |
| 50–60 | Market data, follow-ups, wrap-up |
A 90-minute interview
| 0–8 | Frame, including risk and audit |
| 8–15 | Must do, how well, numbers |
| 15–22 | Nouns, protocol, data flow |
| 22–35 | First working version |
| 35–75 | Deep dives 9.1–9.6 and the final picture |
| 75–85 | Follow-ups: the open, halts, replay for a regulator |
| 85–90 | Trade-offs, metrics, 10× |
| Level | What a strong answer shows |
|---|---|
| Mid-level | An in-memory order book with price-time priority, matching, cancels, acknowledgements and a trade feed. |
| Senior | The sequencer and deterministic single-threaded engine, replication before acknowledgement, sharding by symbol, multicast market data with gap recovery. |
| Staff+ | Determinism as the basis of fairness, failover and audit; fencing; tail latency engineering; volatility controls; the business trade-offs of colocation and second-site loss windows. |
Common mistakes:
- A database on the matching path.
- Floating-point prices.
- Locks and many threads on one book.
- REST and JSON for order entry.
- Market data over one TCP connection per subscriber, with no sequence numbers.
- No answer for "the engine crashed between the fill and the acknowledgement".
13. Try it, check yourself, read more
Order book simulator. Send orders into a small book for one symbol and watch price-time priority work: best price first, then oldest first. Sequence numbers show arrival order.
Prices are stored as integer ticks of 0.01. Try: buy 250 at 100.06 (fills 120 at 100.05, then 130 at 100.06 from order 7, the older one there), then a market sell of 600 (it walks down the bids: 100.03, then 100.02 oldest first, then 100.00).
What does price-time priority mean?
Why must the matching engine be deterministic?
When should an order be acknowledged?
How should market data reach a thousand subscribers?
| Resource | Why |
|---|---|
| The LMAX Architecture (Martin Fowler) | A single-threaded, in-memory, event-sourced trading engine, explained. |
| LMAX Disruptor | The ring buffer that passes events between threads without locks. |
| How to Build an Exchange (talk, YouTube) | Sequencers, determinism and market data from people who run trading systems. |
| FIX standards | The industry protocol for order entry. |
| Aeron | Open-source low-latency messaging with replicated logs, used in trading systems. |