Home › High-Level Design › Design a Stock Exchange (Order Matching Engine)
D

Design a Stock Exchange (Order Matching Engine)

We are building the beating heart of an exchange: a service that takes a flood of buy and sell orders, matches them fairly and deterministically, and confirms fills in microseconds without ever losing an acknowledged order, even if a machine dies mid-trade. This is the rare question that rewards the opposite of your usual reflexes. No sharded database on the hot path. No eventual consistency. A single-threaded in-memory loop you have to defend with a straight face. Walk in with "stick it behind Postgres and cache the reads" and you fail this one in the first five minutes.

High-Level DesignHard16 min read

If you remember five things

The whole answer in one glance. Everything below is the defense of these five calls.

  1. Single writer per symbol. Price-time priority is a total order on one book, so one thread pinned to one core beats any locked multi-threaded design. Contention is the enemy; we delete it by deleting the concurrency.
  2. Sequence first, then match. One sequencer stamps a monotonic number on every event before it reaches the engine. That single serialization point makes races unexpressible and ordering explainable to a regulator.
  3. Durability by replication, not fsync. Ack once the event is in memory on a quorum of log nodes. A disk fsync at 1 to 10 ms never touches the critical path.
  4. Determinism is law. Integer tick prices, no wall clock, no random, no float. The engine is a pure function of the event sequence, so a hot replica stays bit-identical and recovery is pure replay.
  5. Reads are projections. The engine never serves a query. Every view (open orders, depth, history) replays the same log into its own store. CQRS taken to its logical extreme.

1Clarifying the requirements#

Pin down what "exchange" means to this interviewer before drawing a single box. The word covers everything from a crypto toy to NASDAQ, and the two share almost nothing at the core.

Candidate: Are we building just the matching engine, or the whole stack including the brokerage, user accounts, and funding? Interviewer: Focus on the exchange core. Orders arrive already authenticated and risk-checked by an upstream gateway. You own order entry, matching, and the market data feed out.

Candidate: What order types do I need day one? Just limit and market, or stops, icebergs, all-or-none? Interviewer: Limit and market to start. Design so stop orders can be added later.

Candidate: How many symbols, and what latency am I held to? Interviewer: A few thousand symbols. Tail latency under 100 microseconds inside the engine is the bar. And it must be fair: equal price, earlier order wins.

Candidate: Can we lose a single acknowledged order on a crash? And do I need to support cancels and modifies? Interviewer: Zero loss of acknowledged orders. Yes to cancel and modify. Trades, once printed, are final.

What that last answer just handed me:

  • "Zero loss" plus "sub-100us" means durability cannot be a database write in the request path.
  • A single disk fsync is 1 to 10 ms; a cross-zone Postgres commit is tens of ms. Both are 100x to 1000x over the entire engine budget.
  • So durability has to come from somewhere other than "write to the DB and wait." I defend that hard in 6.2.

Functional requirements

  • Accept new orders (limit, market), cancels, and modifies per symbol.
  • Match on strict price-time priority; emit fills (executions) and book updates.
  • Publish a market data feed: top of book, trades, and depth.
  • Replay the full day's order flow to rebuild state (audit and recovery).

Non-functional requirements

  • Determinism: same input sequence produces the identical trade sequence, on any replica.
  • Durability: every acknowledged order survives a process or node crash.
  • Latency: p99 under 100us engine-internal; end-to-end order-to-ack a few milliseconds.
  • Fairness and auditability: a regulator can reconstruct exactly why order X filled before order Y.
From the interviewer's chair

I say the auditability line out loud because it rules out shortcuts later. If a regulator can ask "why did order X fill before order Y," every ordering decision has to be explainable from a durable record, not from "that's the order the threads happened to run in." Name that constraint early and I know you understand the domain, not just the data structures.

2Back-of-the-envelope estimation#

Get the scale in your head, because it decides the architecture. Say we are a mid-size equities venue: 2,000 symbols, 500,000 active trader sessions on a busy day.

The point of this section

You are not here to compute storage to three decimals. You are here to surface the one load-bearing number: a single hot symbol can take 100,000 ordered events per second with zero loss. That is a 10 microsecond budget per event, on a stream that cannot be spread across cores. Find it, and the whole architecture falls out.

Order rate (the number that matters).

  • A normal day is maybe 300 million order events (new, cancel, modify).
  • Over a 6.5 hour session that averages roughly 13,000 events/sec.
  • Markets are not uniform: the open, the close, and news spikes concentrate flow.
  • Plan for a peak of 1 million order events per second across the venue, with one hot symbol capable of 100,000/sec by itself.
  • That burst factor of roughly 75x over the daily average is what bites you: size for the average and you brown out at 9:30am every day.

Trades.

  • Trades are a fraction of orders; most orders cancel.
  • Modern equities flow is market makers quoting and pulling, so the cancel-to-trade ratio runs 10:1 or worse.
  • Call it 30 million trades/day.

Durability write volume.

  • At 1M events/sec, each event maybe 150 bytes serialized, that is 150 MB/sec into the event log at peak.
  • Over a day, the raw order log is a few terabytes.
  • Compressed and tiered to object storage after the session, cheap to keep for years for audit.

Market data out (the real bandwidth hog).

  • If 50,000 subscribers each want the depth feed and we push every book change, fan-out dwarfs ingest.
  • One book change times 50,000 subscribers is a 50,000x amplification on the output side.
  • That lives on a separate multicast and publisher tier, never the matching thread. If it ever touches the matching thread, I have already lost.
100K/sec
The number that drives the designOrdered events on a single hot symbol, zero loss, 10 microseconds each. Price-time priority makes them a total order, so I cannot spread them across cores, and a consensus write per order (a Raft round trip, tens to hundreds of microseconds) cannot keep up. The answer: single writer per symbol, in memory, durability from sequencing and logging, not a synchronous database.

3Core APIs#

Order entry is a binary protocol in real life (FIX or a custom wire format over kernel-bypass networking). For the interview I express it as a clean request shape, but it is a framed binary message, not JSON over HTTP on the hot path.

  • Call this out explicitly: draw a REST box with TLS handshakes in front of the engine and you have added milliseconds to a microsecond budget without noticing.
POST /orders
{
  "client_order_id": "c-8842-00017",
  "symbol": "ACME",
  "side": "BUY",
  "type": "LIMIT",
  "price": "101.25",
  "quantity": 500,
  "tif": "DAY"
}
POST /orders/cancel
{ "client_order_id": "c-8842-00017", "symbol": "ACME" }

Two things worth defending:

  • The client_order_id is the idempotency key. If the gateway retries a send after a timeout, the engine must dedup on it. Otherwise one retry becomes a double buy, which in dollar terms is a real loss and a real incident.
  • Market data is a subscribe, not a request. The engine emits a stream; subscribers tap it off a separate tier.
GET /marketdata/subscribe?symbol=ACME&depth=10

What the engine emits back:

  • ACK (accepted, with an exchange-assigned sequence number and order id).
  • REJECT (with reason), FILL, and BOOK_UPDATE.
  • The ack carries the exchange sequence number, not just "ok." That number is the receipt the trader (and later a regulator) uses to reason about ordering.

4High-level design#

The point of this section

The box diagram is table stakes. What earns points is the spine itself: a durable sequenced input stream, a single-threaded matching core, and a publication tier that never touches matching. The sequencer is the moment of truth. Once an event gets a number and hits the log, it happened, and it happens the same way on every replica.

The spine is a classic LMAX-style pattern, so draw it fast:

  • Everything that touches an order first becomes an immutable, numbered event.
  • A new order lands at the gateway, gets authenticated and risk-checked (buying power, fat-finger limits), then goes to the sequencer.
  • The sequencer stamps a monotonic sequence number, writes to the replicated event log, and only then feeds the engine.
  • The engine mutates the in-memory book, produces fills, and hands results to the publisher.
  • The publisher sends the private fill back to the trader and the public print to the market data feed.
flowchart LR T[Trader] --> G[Order Gateway] G --> R[Risk Check] R --> S[Sequencer] S --> L[(Event Log)] S --> M[Matching Engine] M --> P[Publisher] P --> T P --> F[Market Data Feed] L --> RP[Replica Engine]
The whole system at a glance. The sequencer is the one total-ordering point: it stamps every event and lands it in the replicated log before the engine ever sees it, so the primary and the replica engine consume the identical stream.

Write path and read path are deliberately split.

  • The matching engine never serves a query. It only consumes sequenced events and emits result events.
  • All reads (your open orders, the depth book, trade history) come from downstream consumers that replay the same log into their own query-optimized stores.
  • That separation is what lets the core stay single-threaded and insanely fast.
  • It is CQRS taken to its logical extreme: the write model is one in-memory book, every read model is a projection of the event log.

Why single-threaded.

  • A lock-free structure on one thread, pinned to one core, with the book sized to fit L2/L3 cache, beats any multi-threaded design that needs locks for price-time correctness.
  • A main-memory read is around 100 ns; an L2 hit is a few ns; an uncontended lock plus the cache-line bouncing it causes can cost hundreds of ns and wrecks predictability.
  • Contention is the enemy. We remove it by removing the concurrency.

5Data model#

Use a few stores, each shaped for its own access pattern. The hot path has no SQL, but we still model the persisted log and the downstream query stores, because the interviewer will ask "where does the data actually live" and "what is your SQL vs NoSQL call."

The in-memory book (a pair of structures per symbol):

Structure Representation Notes
Bids Map of price to FIFO queue Price levels sorted descending; best bid is max price
Asks Map of price to FIFO queue Price levels sorted ascending; best ask is min price
Price level Doubly linked list of orders FIFO gives time priority within a price
Order index Hash map order_id to node O(1) cancel and modify by pointer
  • Price levels use an order-statistics structure (a sorted map or a radix tree over tick-sized prices) so "best price" is O(1) and inserting a new level is O(log n).
  • Within a level, a linked list preserves arrival order. Cancel is O(1): the order index hands you the exact node, you unlink it.
  • I like the radix tree over tick prices: valid prices are a bounded, dense integer space (every penny from the floor to the ceiling), so you get array-like access instead of pointer chasing through a tree.

The durable event log (the source of truth):

Field Type Notes
seq int64 Monotonic per partition; the global order
event_type enum NEW, CANCEL, MODIFY, TRADE
symbol string Partition key
client_order_id string Idempotency key
payload bytes Serialized order or fill
ts_exchange int64 Nanosecond stamp assigned by sequencer
  • The log turns the SQL-vs-NoSQL question on its head: it is an append-only, immutable, ordered log, which is neither.
  • Downstream, order history and account positions land in a relational store (strong consistency, you need accurate balances and reporting).
  • The market data depth cache is served from an in-memory read store rebuilt from the log.
  • Trade history for audit goes to columnar object storage, partitioned by day and symbol, because it is write-once and queried by analysts in big scans.
  • Three stores, one source of truth, each shaped for its own access pattern.

6Deep dives#

6.1 The matching engine and price-time priority (the signature hard part)#

This is the core, and the determinism story lives here. Walk through a buy limit arriving at price P:

  • I walk the ask side from the best (lowest) price up.
  • For every resting ask at price <= P, I match against orders in that level in FIFO order until my incoming order is filled or the price no longer crosses.
  • Whatever remains rests on the bid book at its limit price, at the back of that level's queue.
  • A market order is the same walk with no price limit: it sweeps levels until filled or the book runs dry.

Price-time priority is two tiebreaks, in order.

  • Better price always wins.
  • At equal price, the order that arrived first (lower sequence number) fills first.
  • That is why the per-level structure is a FIFO queue, and why the sequencer assigns time, not the wall clock.
  • Two orders in the same microsecond are ordered by sequence number, full stop. Never by a timestamp two machines might disagree on. NTP leaves clocks milliseconds apart, and a millisecond is an eternity here.
sequenceDiagram participant G as Gateway participant S as Sequencer participant L as Event Log participant M as Matching Engine participant P as Publisher G->>S: New buy limit 101.25 x500 S->>L: Append seq 90412 S->>M: Deliver seq 90412 M->>M: Walk asks cross at 101.20 M->>M: Fill 300 then 200 FIFO M->>P: Two fills plus book update P->>G: Private fill confirm P->>P: Public trade print
One order's journey. The sequencer appends and numbers the event before the engine touches it, then the engine crosses against resting asks in FIFO order and hands both fills plus the book update to the publisher.

Determinism is non-negotiable.

  • We run a hot replica that consumes the identical sequenced stream. If primary and replica ever diverge on a single fill, the replica is useless as a standby.
  • So the engine must be a pure function of the event sequence. That bans wall-clock reads, random numbers, hash-map iteration order, and any floating point in price math.
  • We use integer ticks (price in whole cents or finer), fixed-point everywhere.
  • I have watched a design collapse in an interview because the candidate used a double for price and could not explain the drift: 0.1 + 0.2 is not 0.3 in IEEE 754, and if rounding goes one way on one CPU and another way after a compiler reorders an operation, your two "identical" engines print different trades.
  • Integers make the engine bit-reproducible by construction.
Pause before you read on: a trader fires a cancel for order X at the exact instant X is filling on the other side of the book. Who wins, and why is there no race?
Reveal the answer
Whichever event got the lower sequence number. Both the cancel and the fill are events in the one ordered stream. If the fill was sequenced first, the cancel arrives to find X gone and returns "too late to cancel." If the cancel was first, X is pulled and the fill never happens. There is no race because there is one serialization point: the sequencer. The thing that looks like a concurrency bug in any other system is simply not expressible here, because there is no concurrency to race. Reaching for this unprompted is what earns the room.

Trade-off: one symbol cannot borrow a neighbor's core.

  • Single-threaded means a hot streak on one name saturates one thread.
  • We shard symbols across engine instances (A-F on engine 1, G-M on engine 2), so each engine is single-threaded but we scale horizontally by symbol.
  • The cost: a strategy trading a pair across two symbols on two engines has no atomic cross-symbol view.
  • We accept that. Equities match per symbol anyway, and no real equities venue promises cross-symbol atomicity.

6.2 Durability without killing latency#

Here is the tension the requirements set up: zero loss of acknowledged orders, but no synchronous database write in a 10us budget. The resolution:

  • We do not ack the trader until the event is durable.
  • "Durable" means replicated in memory to a quorum, not fsync'd to disk on the critical path.

How it works.

  • The sequencer writes each event to an append-only log replicated to a small set of nodes (say three) over the local network.
  • We ack the order the instant a majority has the event in their log buffer.
  • Those nodes fsync to NVMe asynchronously in batches.
  • Because the event is already on three machines' memory, a single node crash loses nothing: two copies remain, and the engine can replay from any of them.
  • A local network round trip is a handful of microseconds on good hardware, which fits the budget; a disk fsync at 1 to 10 ms does not, so it comes off the critical path entirely.
flowchart LR S[Sequencer] --> L0[(Log Leader)] L0 --> L1[(Log Follower 1)] L0 --> L2[(Log Follower 2)] L0 --> M[Matching Engine] L1 --> D1[(NVMe async)] L2 --> D2[(NVMe async)]
Durability without the fsync tax. The order is acked once a quorum of log nodes has it in memory; each node flushes to NVMe asynchronously in batches, off the critical path.

Recovery is pure replay.

  • A new engine process spins up, reads the event log from sequence 0 (or the last snapshot), and replays every event through the identical matching logic.
  • Because matching is deterministic, replay reconstructs the exact book, down to queue positions.
  • We snapshot the in-memory book every few minutes so replay does not start from the market open.
  • Replay resumes from "snapshot at seq N" and applies N+1 onward.
  • Without snapshots, recovering a 3pm crash means replaying 300 million events, which blows your recovery time. With a snapshot every few minutes you replay at most a few million.

Trade-off: in-memory quorum over synchronous fsync.

  • A simultaneous power loss of all three log nodes before any fsync could theoretically lose the last few milliseconds of events.
  • We make that acceptable by putting the log nodes in different racks with independent power, so correlated loss of all three is astronomically unlikely.
  • This is a consistency call made on purpose: we trade a sliver of theoretical durability for a 100x latency win, and we document exactly what the failure window is.
  • When the interviewer pushes on "but you said zero loss," the honest answer is: zero loss under any single-node failure, and a named, bounded, rack-isolated window for the three-way correlated failure.
Red flag that fails you here

Answering "zero loss" by reaching for a synchronous Postgres commit or a cross-region fsync, then not noticing you just blew the latency budget by 1000x. Or the reverse: hand-waving durability as "the queue handles it" without naming the exact failure window you are accepting. Senior signal is stating the trade precisely: single-node loss is covered, the three-way correlated loss is bounded and rack-isolated, and here is why that is the right call.

6.3 Idempotency and the cancel/modify race#

Cancels and modifies are where correctness quietly breaks. The race resolution is in 6.1: whichever event got the lower sequence number wins, because the sequencer is the one serialization point. This is why I fought so hard for a single writer. Now the send side:

  • The gateway may time out waiting for an ack and resend the same new order.
  • Process both naively and the trader buys twice.
  • So the engine keeps a seen-set of client_order_id per session (a rolling window, since ids are monotonic per client).
  • A duplicate returns the original ack, never a second order.
  • The seen-set is itself rebuilt on replay from the log, so it survives crashes: the dedup state is not a side table we could lose, it is a projection of the same event stream everything else derives from.

Trade-off: the seen-set costs memory per active session.

  • We bound it with a sliding window and reject very old duplicate ids outright rather than remembering forever.
  • The risk is a very delayed retry (seconds late) getting re-processed.
  • That is acceptable because the gateway's own retry timeout is far shorter than the window.

7Bottlenecks and trade-offs#

What breaks first: the single hot symbol.

  • If one name goes vertical on news, its engine thread saturates one core.
  • We cannot shard a single symbol's book across cores without reintroducing the locking we fought to remove, since price-time priority is inherently a total order on that one book.
  • Mitigation is vertical: fastest possible core, book pinned in cache, zero allocation on the hot path (pre-allocated order node pools, no garbage collector pauses, which is why these engines are C++ or Rust or very carefully tuned Java with object pooling).
  • A single GC pause of even 5 ms is 500 events of backlog on a 100,000/sec symbol, so "no allocation on the hot path" is not a nicety, it is a hard requirement.
  • We also throttle at the gateway: a client flooding one symbol gets rate-limited before it reaches the sequencer.

The SPOF: the sequencer.

  • It is the one total-ordering point, so by definition a single active writer.
  • We do not make it multi-master; that would destroy the total order.
  • Instead it is a leader with hot standbys and fast failover. If the leader dies, a standby holding the full replicated log is promoted and continues sequencing from the last committed number.
  • Failover is the scary window: during it, order entry stalls for a few hundred milliseconds.
  • We accept a brief halt over ever producing two conflicting sequences. In an exchange a short pause is fine; a mismatched trade print is a regulatory incident.
  • This is a deliberate CP over AP choice at the sequencer: when the network partitions, we stop taking orders rather than risk two leaders each stamping seq 90412 onto different events.

The consistency call.

  • The matching core is strongly consistent and linearizable by construction (one writer, one order).
  • The read side is eventually consistent. Your "open orders" view served from a downstream store might lag the true book by a millisecond. That is deliberate.
  • We never let a read-side lag affect matching; the engine's in-memory book is the only authority for what fills.
  • A stale read can cost a trader a slightly outdated screen; a stale write would cost someone money and the venue its license.

Market data fan-out is a separate scaling axis entirely.

  • We never let 50,000 subscribers touch the matching thread.
  • The engine emits one stream of book updates; a tree of publisher nodes fans it out, often over multicast on the private network and over WebSocket conflated snapshots to retail.
  • Slow subscribers get conflated updates (latest state) rather than every tick, so one slow consumer cannot back-pressure the core.
  • Conflation is the key trick: a retail phone on 4G gets "here is the current top of book" at 10 updates/sec, while a colocated market maker gets every single tick over multicast, and neither can slow the other down or reach the engine.

Fairness and sub-100us latency demand a single writer. Build durability around that constraint, not against it.

8Summary#

The decisions that matter:

  • Single writer per symbol, in memory. Price-time priority is a total order on one book, so one thread on one core beats any locked design. Contention is the enemy; we delete it by deleting the concurrency.
  • One sequencer, total order. Every event is numbered before it reaches the engine, which makes the cancel/fill race unexpressible and every ordering decision explainable to a regulator.
  • Durability from in-memory quorum replication, not a synchronous disk write, with a named, rack-isolated failure window instead of a hand-wave.
  • Determinism enforced with integer price math and zero wall-clock or random dependence, so a hot replica stays bit-identical and recovery is pure log replay from the last snapshot.
  • Idempotency on client_order_id to survive gateway retries, rebuilt on replay like every other projection.
  • Reads are projections of the log, so the core never serves a query and stays single-threaded and fast.
  • Out of scope by choice: stop-order activation (a conditional queue that injects a deterministic market order on a trigger print), the open and close auctions (a batch cross at a single clearing price, a different algorithm from continuous matching), and kernel-bypass networking plus NUMA pinning. Naming them keeps me honest about what the time covered.
Follow-ups the interviewer will ask (and your answers)
"You said zero loss, but you do not fsync before acking. Explain."
Zero loss under any single-node failure: the event is in memory on a quorum (say three log nodes) before I ack, and fsync to NVMe happens asynchronously in batches off the critical path. The only loss scenario is a simultaneous power loss of all three nodes before any fsync, which I bound by putting them in separate racks with independent power. A local network round trip fits the 10us budget; a 1 to 10 ms fsync does not. I trade a sliver of theoretical durability for a 100x latency win, and I state the exact window.
"A cancel and a fill for the same order arrive at the same instant. Who wins?"
Whichever got the lower sequence number. Both are events in one ordered stream, and the sequencer is the single serialization point. Fill sequenced first: the cancel finds the order gone and returns too-late. Cancel first: the order is pulled and the fill never happens. No locks, no race, because there is no concurrency to race.
"Why not a distributed database or Raft per order for durability?"
A consensus write per order is a network round trip of tens to hundreds of microseconds minimum, which cannot keep up with a 10us-per-event budget on a hot symbol doing 100,000/sec. I get durability instead from sequencing plus in-memory quorum replication of an append-only log, which fits the budget, and recovery from deterministic replay off that log.
"Your two replicas printed different trades once. How is that even possible?"
Non-determinism leaked in. Almost always floating-point price math (0.1 + 0.2 is not 0.3 in IEEE 754, and rounding can differ across CPUs or compiler reorderings), or a wall-clock read, a random number, or hash-map iteration order. The fix is to make the engine a pure function of the event sequence: integer tick prices, fixed-point everywhere, no clock, no random. Then replay and the replica are bit-identical by construction.
"The gateway retries a send after a timeout. Does the trader buy twice?"
No. The engine keeps a seen-set of client_order_id per session in a sliding window; a duplicate returns the original ack instead of creating a second order. The seen-set is a projection rebuilt on replay from the log, so it survives crashes. We bound it with the window and reject very old duplicates, which is safe because the gateway's own retry timeout is far shorter.

What you can now explain in the room

  • Why fairness and sub-100us latency force a single-threaded, single-writer-per-symbol engine instead of a sharded database.
  • How one sequencer turns the cancel/fill race into a non-race and makes ordering explainable to a regulator.
  • How to hit zero loss without an fsync on the critical path, and the exact failure window you are accepting.
  • Why integer tick prices and a ban on wall clocks and floats keep a hot replica bit-identical.
  • How recovery is pure deterministic replay from the last snapshot, not a database restore.
  • Why market data fan-out and all reads live off the engine entirely, as projections of the same log.

For a payments-flavored take on atomicity and idempotency under money movement, see our UPI and digital wallet design guide.

Get a prep plan built around your Design a Stock Exchange (Order Matching Engine) interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Design a Stock Exchange (Order Matching Engine) prep plan is on the way.