System design interview
Design a stock exchange matching engine
Order books, price-time priority, a sequencer, hot standby and market data fan-out.
At a glance
- Advanced
- Consistency, Real-time
- Order gateway · Risk check · Sequencer · Market data · Event log · Matching engine · Standby engine
Press Play to follow a request through the design, or step through it.
- Client and edge
- Service
- Data store
- A broker sends a limit order: buy 220 shares of one symbol at 10.03.
- The gateway authenticates the broker and passes a parsed order to the risk check.
- The risk check approves it. The price is inside the band, and the order adds 220 × 10.03 = 2,206.60 to exposure, under the limit.
- The sequencer stamps it with the next sequence number and sends it to the primary matching engine.
- At the same time it writes the order to the replicated log. Results are held back until that write is safe.
- The standby engine reads the log and applies the same order to its own copy of the book.
- Brokers
- Order gateway: auth, parse
- Risk check: limits, exposure
- Sequencer: one global order
- Market data: builds the feed
- Event log: replicated
- Matching engine: one thread/symbol
- Subscribers
- Standby engine: same book
On this page
In 2 minutes
- The numbers. The compute is small. One thread has 8 times the headroom it needs for the busiest symbol.
- Threading. One thread per symbol, no locks.
- What to replicate. The ordered input log, with snapshots.
- Numbers. Whole-number ticks and shares.
- Distributing market data. Broadcast once, with sequence numbers and resend on a gap.
Read the 2-minute answerHide the 2-minute answer
The compute is small. Even the busiest symbol at 25,000 messages a second uses 12.5% of one core, so the design is about order, fairness and failover. Brokers connect to a gateway, then a risk check applies price bands, exposure limits and self-trade rules before an order can reach the book. A sequencer gives every message the next sequence number, sends it to the matching engine and writes it to a log replicated to three nodes.
The engine runs one thread per symbol and is a pure function of the sequenced input, using whole-number prices, so replaying the log gives identical trades. The book keeps a FIFO queue per price level and a map from order ID for constant-time cancels. A hot standby replays the same log and builds the same book. If the primary stops, an epoch number fences it off, and the standby takes over within a second with no gap and no duplicate, since outputs are deduplicated by sequence number.
Results return through the sequencer to the sender, and a publisher broadcasts market data once, with sequence numbers and resend on gaps, because sending to each subscriber would need 200 Gbit/s. The cost is strict determinism everywhere.
Step 1 of 6: Clarify the scope
Questions to ask first
Ask these before you draw anything. Guess why each answer changes the design, then open it to check.
Which order types do we support: limit, market, cancel?
How many symbols and how many messages a second at the peak?
What latency do we target, and does fairness matter more than raw speed?
What happens when a machine fails in the middle of trading?
Do all participants get market data at the same moment?
Requirements
Functional
- Accept limit, market and cancel orders for each symbol.
- Match by price-time priority: the best price first, and the earliest order first at the same price.
- Send the sender an execution report for every change to their order.
- Publish every trade and every change to the book to all subscribers.
- Reject bad orders before they reach the book, using risk checks.
Non-functional
- From order in to acknowledgement out: p99 under 1 ms.
- The same input sequence always produces exactly the same trades.
- No accepted order is lost: it survives the loss of any one machine.
- A standby takes over within 1 second, with no gap and no duplicate trade.
Step 2 of 6: Size it
Show the mathsHide the maths
These numbers are assumptions for the exercise, not facts about a real system:
- There are 5,000 symbols. The peak is 500,000 inbound messages a second, counting new orders and cancels. The busiest symbol gets 5% of them.
- The session runs 6.5 hours (23,400 s) and averages 100,000 messages a second.
- Matching one message takes about 5 microseconds on one core. A message is 64 bytes.
- A book holds about 4,000 resting orders. A market data update is 50 bytes, and there are 1,000 subscribers.
The arithmetic:
- Load on the busiest symbol
- 500,000 × 5%
- 25,000 messages a second
- CPU for that symbol
- 25,000 × 5 µs = 125 ms of work per second
- 12.5% of one core, which can do 200,000 a second
- Latency budget
- Gateway 50 + risk 20 + sequencer 10 + log copy 100 + match 5 + out 50 µs
- 235 µs, inside the 1 ms goal
- Input log per day
- 100,000/s × 23,400 s = 2.34B messages × 64 B
- ≈ 150 GB a day
- Memory for all books
- 4,000 orders × 64 B = 256 KB per symbol; × 5,000
- ≈ 1.3 GB, and a hot book fits in a core's cache
- Market data, if sent one by one
- 500,000 × 50 B = 25 MB/s = 200 Mbit/s; × 1,000 subscribers
- 200 Gbit/s, so one broadcast copy is needed
Step 3 of 6: Sketch the path of one request
Step through the diagram at the top of the page to watch a request move through these parts.
Brokers connect to the Order gateway, which authenticates them and turns their messages into a standard form. Each order next goes through the Risk check, which rejects anything outside the participant's limits before it can touch the book.
The Sequencer is the single point that decides the order of events. It gives every accepted message the next sequence number, sends it to the primary Matching engine, and writes it to the replicated Event log. The engine is single-threaded for each symbol and is a pure function of the sequenced input: the same input always gives the same trades. A Standby engine reads the same log and builds an identical book.
The engine's results come back through the sequencer, which numbers them too. They go two ways. Execution reports go back along the path of the order to the sender, and update the risk check's view of exposure. The Market data publisher broadcasts trades and book changes to the Subscribers.
Other paths through the system
Report to the sender
- The engine matched the order against the book: 150 shares traded at 10.02 and 70 at 10.03, so it is filled. It returns those events to the sequencer.
- The sequencer numbers the events and sends them back. The risk check moves the filled quantity from open buys into position, at the fill prices.
- The execution report passes on to the gateway that holds the broker's connection.
- The broker receives the fills and the remaining quantity, in sequence order.
Publish market data
- The same sequenced events include two trades and a change: the best offer is now 130 shares at 10.03.
- The sequencer hands them to the market data publisher, with their sequence numbers.
- The publisher broadcasts each update once. Subscribers use the sequence numbers to spot a gap and ask for a resend.
Step 4 of 6: Go deep on the hard part
The order book and price-time priority
What data structure holds the book, and how does an incoming order match?Each symbol has its own book with two sides. Bids (buys) are sorted from the highest price down. Asks (sells) are sorted from the lowest price up.
Each symbol has its own book with two sides. Bids (buys) are sorted from the highest price down. Asks (sells) are sorted from the lowest price up. Prices and quantities are whole numbers, in ticks and shares, never floating-point, so there is no rounding and results are identical on every machine.
Each price level holds a first-in, first-out queue of resting orders. That is the time priority. The book keeps the levels in a sorted structure, or in an array indexed by tick when the price band is bounded. A hash map from order ID to the order's place in its queue makes a cancel a single step, with no search.
Matching a limit order. An incoming buy at price P looks at the best ask. While it has quantity left and the best ask is at or below P, it trades with the oldest order at that level. The trade happens at the resting order's price. Whatever is left rests at the back of its own level.
A worked example. The asks are 100 shares (A) at 10.02, 50 (B) at 10.02, and 200 (C) at 10.03. A buy of 220 at 10.03 arrives. It trades 100 with A and 50 with B, both at 10.02. It then trades 70 with C at 10.03, leaving C with 130. The buyer asked to pay up to 10.03 but got 150 shares at the better price of 10.02.
A market order has no price. It takes whatever the book offers until it is filled or the book is empty, and any rest is cancelled; it never rests. A cancel finds the order by ID and unlinks it. If the order was already filled, the cancel is rejected as too late. Whichever of the two was sequenced first wins, which is what makes the answer fair.
Cutting the quantity of a resting order keeps its place in the queue. Raising it, or changing the price, sends it to the back. Matching costs time in proportion to the number of trades it makes. Cancels, which are most of the traffic, are constant time.
The sequencer, a single thread and the standby
How do you make matching deterministic and fast, and survive a failure of the engine?Matching must be a pure function of an ordered stream: new book = match(old book, next event).
Matching must be a pure function of an ordered stream: new book = match(old book, next event). If the order of events is fixed, the result is fixed. So one component, the sequencer, decides the order. It stamps every message with the next sequence number. That is the answer to 'who was first', and it removes ties.
One thread per symbol. With a single writer there are no locks and no races. The book fits in the core's cache, about 256 KB, and the loop avoids memory allocation and system calls. At about 5 microseconds a message, one thread handles 200,000 a second, against 25,000 needed for the busiest symbol. To scale, spread symbols over many engine threads or machines. Never share one symbol between threads.
The replicated log. The sequencer appends each event to a log that is copied to three nodes, and it counts as written when two have it. The engine does not release a result until its input is safe. The engine's state is just a cache of the log. A snapshot of the book plus its sequence number is saved every few minutes, so recovery replays only the tail.
The hot standby reads the same log and applies every event in the same order, so its book is identical. It discards its outputs while it is not in charge. If the primary stops answering for, say, 200 ms, the standby takes over. First fence off the old primary: every output carries an epoch number, and a lower epoch is ignored downstream, so a primary that is only slow cannot emit stale results. Then replay any events past what it applied. Because the engine is deterministic, the results it regenerates are byte for byte what the old primary produced, and anything already published is dropped by its sequence number.
Do not read the wall clock in the matching code. Timestamps are assigned by the sequencer and carried in the event, so a replay is exact. The sequencer is a single point too, so run it as a primary and a backup with the same epoch rule, continuing from the last sequence number in the log.
Risk checks and market data
What stops a bad order from reaching the book, and how does one engine feed a thousand subscribers fairly?Risk checks run before the sequencer, so a rejected order takes no sequence number and never touches the book.
Risk checks run before the sequencer, so a rejected order takes no sequence number and never touches the book. They are in-memory counters per participant, so each check takes microseconds.
- Fat-finger limits. Reject a price far from a reference, say outside 10% of the last trade, or a quantity above a cap, say 1 million shares.
- Exposure. Keep a worst-case figure per participant. For buys, it is the open buy quantity times each order's limit price, plus the value of the current position at the last trade price. Add the new order's quantity times its price, and reject it if the total passes the participant's limit. Sells are checked the same way against short exposure. A fill turns open quantity into position at the fill price, and a cancel or a sell lowers the figure.
- Rate caps. Limit messages a second per connection, so one runaway program cannot flood the engine.
- Self-trade prevention. Do not match a participant against itself. Cancel the older or newer order by a stated rule.
- A kill switch. One command cancels all of a participant's orders and blocks new ones.
Market data. The publisher turns the sequenced events into two streams. The incremental stream carries every change to the book and every trade, each with a sequence number. A snapshot stream repeats the top levels of each book every second or so, so a late joiner can catch up.
Sending 25 MB a second to each of 1,000 subscribers one by one is 200 Gbit/s, which no single publisher can do. Instead send each update once to a network that copies it, such as multicast inside the data centre, with a distribution tier beyond it. Subscribers watch the sequence numbers. On a gap, they ask a resend server for the missing range or wait for the next snapshot.
Fairness. Everyone should see events in the same order and at about the same time. Because outbound events also pass through the sequencer, there is one global order for reports and public data alike. Give co-located participants cables of equal length, so geography does not decide who is first. The event log doubles as the audit trail, kept for as long as regulators require.
Step 5 of 6: Name the trade-offs
- One thread per symbol, no locks
- A shared book with many threads and locks
It is simple, fast and deterministic. The cost is that one symbol is limited by one core, so we shard symbols and watch the busiest one.
- The ordered input log, with snapshots
- Copying the engine's state after every change
Input events are small, and any machine can rebuild the book by replaying them. The cost is that the engine must be strictly deterministic.
- Whole-number ticks and shares
- Floating-point prices
No rounding differences between a primary and a standby. The cost is converting at the edges, and care with the price scale.
- Broadcast once, with sequence numbers and resend on a gap
- A separate connection to each subscriber
The cost of the feed does not grow with the number of subscribers, and everyone gets the same stream. The cost is gap handling in every client.
Step 6 of 6: Say what breaks next
Follow-up questions an interviewer asks once the design is on the board. Answer each one out loud, then open it to compare.