Blueprint
Money & markets advanced

Design a stock exchange

A microsecond hot path where determinism beats parallelism — and where reaching for a database or a thread pool is how strong candidates fail.

asked at Bloomberg · Coinbase · Databento Rarely asked ~40 min walkthrough
01

Scope the requirements

First move: pin the scope, because "design a stock exchange" and "design Robinhood" are different interviews. I'm building the exchange itself — the matching venue — not a retail broker. Second move: notice this inverts the usual playbook. No caches, no horizontal fan-out on the hot path; the game is latency, determinism, and fairness.

Functional — what it must do

  • Place and cancel limit orders (market orders modelled as aggressive limits); a risk check on funds and limits gates every acceptance.
  • A matching engine matches orders by price-time priority — best price first, FIFO among equal prices — producing executions.
  • Execution reports stream back to clients: new → partially filled → filled / cancelled.
  • Publish market data: L1 best bid/ask, L2 order-book depth, and candlestick aggregates.
  • Scope: ~100 symbols, one exchange, equities only, continuous trading hours.
  • Explicitly cut: opening/closing auctions, options, self-trade prevention and circuit breakers — I name them as extensions.

Non-functional — the numbers that shape the design

  • Latency: matching in single-digit microseconds; client wire-to-wire in low milliseconds. The tail matters more than the mean — a stable p99 is the product.
  • Throughput: ~43,000 orders/sec average, ~215,000/sec at peak.
  • Determinism: the same input sequence must produce the same output, every time, on every replica — this is both a fairness and a recovery requirement.
  • 99.99%+ availability across the 6.5-hour trading day, with failover fast enough that the market barely blinks.
  • Security at the edge: a DDoS-resistant public boundary and strict validation before anything touches the matching core.
Say this out loud

The unusual thing about this design is that almost every standard scaling tool is banned from the hot path — caches, sharding a symbol, thread pools all destroy either latency or determinism. So my architecture is going to look strange on purpose: single-threaded, in-memory, one box.

02

Back-of-envelope estimates

The numbers here don't size a database — they justify why there isn't one on the hot path, and they set the latency budget everything else must live inside.

QuantityEstimateHow you got there
Order rate~43k/s avg1B orders/day ÷ 6.5 trading hours (~23,400 s); peak 5× ≈ 215k/s.
Cancels~half of all trafficmarket makers constantly re-quote; cancel must be as cheap as place — this drives the data structure choice.
Event log~100 GB/day1B events × ~100 B, append-only, retained for replay and audit.
Order book size10k–1M resting orders/symbolat ~100 B each that's ≤100 MB per symbol — all 100 books fit in one machine's RAM with room to spare.
Latency budgetµs, not msone cross-machine network hop is ~50–500 µs — which is the whole budget. Hence: no hops inside the critical path.
Pricesinteger ticksprice as an integer count of the tick size (e.g. cents) — float rounding on money is disqualifying here.
Say this out loud

215k events a second is nothing for memory and everything for a database — even a 1 ms store gives me a 200-deep queue growing every second at peak. The log is my durability; RAM is my working state.

03

Sketch the API

In reality this boundary speaks FIX over dedicated lines; I'll present it as REST for clarity and say so. The interesting design fact is that responses are acknowledgements, not outcomes — outcomes arrive asynchronously as execution reports.

POST/v1/ordersbody: { symbol, side, price, quantity } → { orderId, status: accepted }. Acceptance means risk-checked and sequenced — not matched.
DELETE/v1/orders/{orderId}cancel. Half the traffic and latency-critical: a slow cancel leaves a market maker exposed at a stale price.
GET/v1/marketdata/orderbook/{symbol}?depth=L2 snapshot — top N price levels with aggregate quantity per level. Streaming updates via the market-data feed, not polling.
GET/v1/marketdata/candles?symbol=&resolution=&start=OHLC candlesticks from the time-series store, built off the trade stream.
  • Execution reports are pushed, not polled: fills, partial fills, and cancel confirmations stream to the client over a persistent connection (WebSocket here; FIX drop copy in reality). The POST response can't contain the outcome because the outcome may be a sequence of partial fills over time.
  • Real exchanges publish market data via multicast UDP so every co-located subscriber receives ticks simultaneously — fan-out via TCP would make fairness a function of your position in the send loop. Worth saying even in the REST framing.
04

Data model

Two structures carry the design: an in-memory order book tuned so every operation the traffic mix demands is O(1), and a sequenced event log that replaces the database entirely.

order book (per symbol, in memory)
sorted price levels (tree/array) · each level a FIFO doubly-linked list of orders · hashmap orderId → list node
match at best price O(1), add O(log P) in the price levels, cancel O(1) via the hashmap — the FIFO list is the time priority.
sequenced event log
seq (monotonic) · event type · order fields · ts (append-only)
the durable store. Book state = a deterministic fold over the log; there is no other source of truth.
candles / analytics
symbol · window · OHLC · volume
time-series store fed off the market-data stream — completely off the hot path.
The database call

The deliberate call is that the log is the database — event sourcing, not a schema. Every mutation is an appended event; the order book is derived state any replica can rebuild by replaying. Postgres exists in this system for accounts and reference data, never for orders in flight. There is no scale at which I'd move matching into a database — a 1 ms round trip is three orders of magnitude over budget — but I would happily use one for everything outside the trading day.

05

High-level design

The shape: a hardened gateway at the edge, then a critical path — risk, sequencer, matching — that lives on one machine communicating through shared-memory ring buffers, LMAX-style. The sequencer stamps every event; the log feeds a warm replica and the market-data fan-out.

client edge matching core data market data Trader broker / market maker Client gateway auth, validation, FIX/REST Risk manager funds + limits Sequencer single writer, monotonic ids Matching engine single thread / symbol Event log sequenced, append-only Warm replica replays the log Market data publisher L1/L2 feed Candle builder time-series new / cancel validated append seq'd events replay exec reports L2 feed trades + book
notice the solid path from gateway to matching never touches a database or crosses a machine — risk, sequencer and matching share one box via ring buffers; everything dashed hangs off the sequenced stream.
  1. A new order hits the gateway: authentication, schema validation, rate limits — the untrusted world ends here.
  2. The risk manager checks funds and position limits in memory and passes the order on; rejects never reach the core.
  3. The sequencer — a single writer — stamps the order with a global monotonic sequence number and appends it to the event log. From this point the event is durable and its order in history is settled.
  4. The matching engine (one thread per symbol, no locks) applies the event to the book: match against the opposite side at price-time priority, or rest it. Executions flow back through the sequencer to be stamped as outbound events.
  5. Execution reports return to clients via the gateway; the market data publisher fans trades and book deltas out to the L2 feed and the candle builder. The warm replica consumes the same log and stays seconds-fresh for failover.
06

Deep dives — where the interview is won

Phase 01

The order book: hashmap plus FIFO lists, because cancels are half the traffic

The traffic mix dictates the structure. Adds need ordered prices: keep price levels in a sorted structure — a balanced tree, or better, a contiguous array indexed by tick, since real books cluster tightly around the touch and arrays are kind to caches. Time priority within a level needs FIFO: a doubly-linked list of orders per level, matched from the head. And cancels — roughly half of all messages, because market makers re-quote constantly — need O(1): a hashmap from orderId to its list node, so a cancel unlinks in constant time without scanning anything.

This is why a priority queue alone fails the question: heaps give you the best price but make arbitrary-order cancellation O(n), which dies at 100k cancels/sec. And it's why any database representation fails harder — matching one incoming order can touch dozens of resting orders, and at microsecond budgets even a 1 ms round trip per touch is a thousand times over budget. The book lives in the matching thread's memory; at ≤100 MB per symbol, all hundred books fit in RAM trivially.

bids asks $100 [O1, O2] $99 $101 [O5] $102 cross ↑
one hashmap per side, price → FIFO list. cancel is O(1); match is O(1) at top.
Trade-offA bespoke in-memory structure means durability and queryability come from somewhere else — the event log — so I'm signing up for event sourcing as a package deal, not as a flourish.
Phase 02

The sequencer makes time official, and the log becomes the database

Distributed systems argue about ordering; an exchange cannot. The sequencer is a single writer that stamps every inbound event — and every outbound execution — with a global monotonic sequence number and appends it to the log. That stamp is the definition of what happened first, which is the exchange's fairness contract made mechanical: price-time priority is meaningless if "time" is negotiable.

Combined with a deterministic, single-threaded matching engine, the sequenced log becomes the entire persistence story: state is a pure fold over events, so any process replaying the log reaches bit-identical state. That one property pays three times — recovery (rebuild by replay), HA (a warm replica is a continuously-replaying follower), and audit (regulators can re-derive any historical moment). The sequencer is obviously a serialisation point, so it must do almost nothing: stamp, append, hand off. LMAX demonstrated this shape processing 6M events/sec on one thread — 215k/s leaves enormous headroom.

orders sequencer log Orders Sequencer assigns seq_no Sequenced log the DB append
sequencer stamps every order. log of stamped orders IS the exchange state — replayable, deterministic.
Trade-offA single sequencer caps total throughput and is a single point of failure — I accept the cap because it's ~30× above peak, and I cover the failure with log-replay failover rather than by distributing the one thing that must stay serial.
Phase 03

Microsecond engineering: one box, one thread, no locks, no GC, no hops

The naive version puts risk, sequencer, and matching in separate services — and each network hop spends 50–500 µs, several times the entire budget. The strong answer co-locates the critical path on one server, with stages communicating over shared-memory ring buffers (mmap), so a message passes between stages in nanoseconds. This is the LMAX Disruptor architecture, and naming it lands well.

Within the process, the discipline is: one pinned thread per stage (no locks, no context switches — locks introduce scheduling jitter that destroys the p99), pre-allocated object pools (no malloc, and no GC pause in the hot loop — a stop-the-world pause is a market-wide halt), and kernel-bypass networking (DPDK-class) at the edge so packets skip the kernel stack. The meta-point to say out loud: this is the opposite of web-scale reflexes. Parallelism is the enemy here, because parallelism is nondeterminism — you go fast by making one core do very little, perfectly predictably.

one box NIC (kernel bypass) Single thread no locks Order book in RAM µs
match engine: pinned single thread, no gc pauses, no network hops, kernel-bypass NIC.
Trade-offVertical scaling has a hard ceiling and demands specialist engineering — the escape hatch is sharding by symbol across boxes, which preserves per-symbol determinism, never by splitting one symbol's flow.
Phase 04

Failover is replay, not restore

The classic follow-up: the matching engine dies mid-batch — what now? Because state is a deterministic function of the sequenced log, the answer is mechanical: the warm replica has been consuming the same log all along and holds a book seconds behind at worst. Failover is promote, replay the gap from the last consumed sequence number, resume. Events are idempotent by sequence number — apply-once is enforced by "is this seq the next one?" — so replaying over a partial batch is safe by construction. No backup-restore, no cache warm-up, no "did the last write commit?" ambiguity.

Cold start is the same operation with a longer gap: replay from the start of day (or a snapshot plus tail). Snapshots are an optimisation of replay, not a second source of truth. The one thing that must survive is the log itself, so it's the only component that pays for synchronous replication — everything downstream of it is reconstructable.

primary log hot spare Match engine A Sequenced log Match engine B replays log write tail
hot spare listens to the sequenced log. failover = new leader picks up at last committed seq_no.
Trade-offDuring the replay gap the market is halted for those symbols — seconds of downtime traded for the guarantee that the recovered book is provably identical to the one that died, which is the trade every real exchange makes.
07

Follow-ups you should expect

Why not distribute matching for one symbol across several machines?
Because matching is order-sensitive: two nodes matching AAPL need a total order over incoming events, and imposing one costs consensus latency plus nondeterministic outcomes on ties — you'd rebuild the sequencer, slower and wrong. Shard by symbol instead: each symbol's flow is fully serial on one engine, engines scale horizontally, determinism per symbol is preserved.
Why is single-threaded faster here? That sounds backwards.
The workload is tiny per event — a few pointer operations on hot in-cache structures — so the cost of coordination dominates the cost of work. Locks add contention and scheduler jitter; a pinned lock-free thread keeps everything in L1/L2 and delivers a flat p99. LMAX proved the pattern at 6M events/sec on one thread. Throughput isn't the constraint; tail latency and determinism are, and both get worse with threads.
How do market orders and partial fills work in this model?
A market order is a limit order at the most aggressive representable price — it walks the opposite side of the book level by level, generating one execution per resting order it consumes, until filled or the book is empty. Partial fills fall out naturally: each execution decrements the resting order's quantity, and the remainder keeps its original time priority. Each execution is its own sequenced outbound event.
How do you keep HFT firms from having an unfair advantage?
You can't remove the speed race, so exchanges make it symmetric: co-location sold with equalised cable lengths, and market data published via multicast UDP so all subscribers receive ticks at the same instant. If the interviewer wants structural fixes, frequent batch auctions — match once every few hundred milliseconds — kill the latency race entirely, at the cost of continuous price discovery.
Where does a database actually appear in this system?
Everywhere the trading day isn't: accounts, positions and reference data in Postgres; candles and analytics in a time-series store; the event log archived to object storage for compliance. End-of-day settlement folds the day's sequenced log into the books of record. The rule is that anything with a microsecond budget never waits on anything with a millisecond one.

Where candidates lose the room

  • Putting the order book in Postgres or Redis and matching via queries — the single most common failure; it misses the entire point of the latency budget by three orders of magnitude.
  • Multi-threading the matching engine with locks — nondeterminism means replicas diverge, replay stops working, and two runs of the same day disagree; the whole recovery story collapses.
  • Ignoring cancels — they're ~half the traffic, and a design without O(1) cancel (the orderId hashmap) falls over on the first market-maker re-quote storm.
  • Float prices — money in binary floating point drifts; integer ticks are the only acceptable representation and interviewers listen for it early.
  • Asserting "we run a hot standby" with no replay mechanism — HA without event sourcing is a claim, not a design; the sequenced log is what makes failover provable.

Concepts this leans on

Found a bug or want more?

Tell me what's broken, missing, or wrong. Every message lands in my inbox.