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.
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.
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.
| Quantity | Estimate | How you got there |
|---|---|---|
| Order rate | ~43k/s avg | 1B orders/day ÷ 6.5 trading hours (~23,400 s); peak 5× ≈ 215k/s. |
| Cancels | ~half of all traffic | market makers constantly re-quote; cancel must be as cheap as place — this drives the data structure choice. |
| Event log | ~100 GB/day | 1B events × ~100 B, append-only, retained for replay and audit. |
| Order book size | 10k–1M resting orders/symbol | at ~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 ms | one cross-machine network hop is ~50–500 µs — which is the whole budget. Hence: no hops inside the critical path. |
| Prices | integer ticks | price as an integer count of the tick size (e.g. cents) — float rounding on money is disqualifying here. |
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.
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.
- 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.
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.
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.
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.
- A new order hits the gateway: authentication, schema validation, rate limits — the untrusted world ends here.
- The risk manager checks funds and position limits in memory and passes the order on; rejects never reach the core.
- 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.
- 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.
- 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.
Deep dives — where the interview is won
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.
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.
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.
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.
Follow-ups you should expect
Why not distribute matching for one symbol across several machines?
Why is single-threaded faster here? That sounds backwards.
How do market orders and partial fills work in this model?
How do you keep HFT firms from having an unfair advantage?
Where does a database appear in this system?
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.