Design problems
Payments & fintechHardSDE-3 35 min

Design a stock exchange order-matching engine

Asked at
Goldman SachsGrowwNVIDIAZerodha

The interviewer asks

“Design the core of a stock exchange: brokers send buy and sell orders, the engine matches them by price-time priority, and every participant gets trade confirmations and market data. It must be fair, deterministic and extremely fast.”

What a strong answer looks like

A strong answer puts a single-threaded, in-memory matching engine per symbol partition at the centre, fed by a sequencer that gives every inbound message a global order and writes it to a replicated journal before matching. Because matching is a deterministic function of the ordered input, replicas that replay the same journal reach the same state, which gives fault tolerance without distributed locks. The order book is a pair of price-sorted ladders with FIFO queues per price level, so add, cancel and match are O(1) or O(log n). Around the core sit gateways (session handling, validation, throttling), pre-trade risk checks, a market data publisher (multicast, tick-by-tick plus snapshots) and a drop-copy/clearing feed. The interview is won on determinism, latency in microseconds, and how you fail over without losing or duplicating a single trade.

Concepts to mention

Price-time prioritySingle-threaded matching per symbol partitionSequencer and append-only journalDeterministic replay for replicas and recoveryOrder book as price levels with FIFO queues and an order-id indexInteger prices in ticks, no floating pointHot standby with output deduplicationUDP multicast market data with A/B feeds and sequence gap recoverySnapshots plus incremental updatesPre-open call auction

1 Requirements

Functional

  • Accept new, modify and cancel orders from member brokers over a session protocol (FIX or a binary protocol)
  • Support limit, market, immediate-or-cancel (IOC) and stop-loss orders
  • Match by price-time priority: best price first, then earliest order at that price
  • Send execution reports (ack, partial fill, fill, cancel, reject) to the owning broker
  • Publish market data: every order book change (tick-by-tick) and periodic snapshots of top-N depth
  • Enforce price bands (circuit limits) and halt or resume trading per symbol
  • Run a pre-open call auction to discover the opening price

Non-functional

  • Matching latency (inbound message to execution report out) p99 under 100 µs; median in low tens of µs
  • Throughput: 1M+ inbound messages/s across the exchange at peak; a single hot symbol up to ~200k msgs/s
  • Fully deterministic: the same input sequence always produces the same trades
  • No lost or duplicated orders or trades, even on node failure; recovery in seconds
  • Fairness: no participant can see or act on information before others on the same feed
  • 99.99%+ availability during trading hours with a hot standby and a disaster recovery site

Out of scope (say it out loud)

  • Clearing, settlement and margining at the clearing corporation
  • Broker-side apps, OMS and retail UIs
  • Co-location data centre physical design
  • Regulatory surveillance analytics (mentioned only as a consumer)

2 Estimates

Symbols~10k active order booksA few thousand equities plus many F&O contracts; each strike/expiry is its own book
Inbound messages~1M/s peak, ~10B/day possibleAlgo traders send many modifies and cancels per trade; order-to-trade ratios of 50-100:1 are common
Trades~20k-50k trades/s peakMost messages do not trade; ~2-5% of messages result in an execution
Order book memory~1-5 GB in total~10M resting orders × ~100 bytes ≈ 1 GB; fits easily in RAM on each engine node
Journal write rate~100-200 MB/s at peak1M msgs/s × ~100-150 bytes per sequenced message
Market data out~1-2M updates/sEach order add/modify/cancel/trade changes the book and becomes one tick-by-tick message
Latency budget~50 µs internalGateway decode 5 µs + sequencer + journal replicate 15-20 µs + match 1-5 µs + report encode 5 µs; network hops inside the DC are a few µs each

3 API

EndpointWhat it does
FIX/binaryNewOrderSingleclient_order_id, symbol, side, qty, price, order_type, time_in_force, account
FIX/binaryOrderCancelReplaceRequestModify price or quantity of a resting order (price change or quantity increase loses time priority)
FIX/binaryOrderCancelRequestCancel a resting order by exchange order id or original client_order_id
FIX/binaryExecutionReportEngine to broker: ack, reject (with reason), partial fill, fill, cancelled; carries exchange order id, trade id, fill price and qty
Multicast UDPTick-by-tick feedSequenced order-level updates (add, modify, cancel, trade) per channel; A/B duplicate streams
TCPRetransmission / snapshot serviceRecover missed multicast sequence numbers or fetch a full book snapshot to resync
TCPDrop copyCopy of every execution to the broker's risk systems and to the clearing corporation

4 Data model

Order book (per symbol)In-memory (engine process)
bids: price-sorted levels (desc)asks: price-sorted levels (asc); each level = FIFO doubly linked list of orders with total qty

Prices are integers in ticks (paise), never floats. Levels in an array indexed by tick offset within the price band, or a tree map. Orders also in a hash map order_id → node for O(1) cancel.

OrderIn-memory, pooled objects
exchange_order_idmember_idclient_order_idsideprice_ticksopen_qtyfilled_qtyentry_seqflags

Preallocated object pools avoid garbage collection pauses. entry_seq gives time priority.

Sequenced journalAppend-only file on local NVMe, replicated to standby and DR
global_seqpartitiontimestampraw inbound message

The single source of truth. State = replay(journal). Rolled daily, archived to object storage for audit.

Engine snapshotsLocal disk + object storage
partitionlast_applied_seqserialized books

Taken periodically (for example every few minutes, or at end of day) so recovery replays only from the snapshot, not from the day start.

TradesJournal-derived; downstream in a relational DB / column store
trade_idsymbolbuy_order_idsell_order_idbuyer_memberseller_memberpriceqtyseqts

Produced deterministically from the journal; clearing, surveillance and reporting consume a stream of them.

Instrument and member reference dataLoaded into memory at start of day
symboltick_sizelot_sizeprice_bandstatus; member limitssession ids

Changes during the day (a halt, a band change) are themselves sequenced commands, so replay stays deterministic.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALorderspersist + replicateordered streamsame streamexecution reportsbook eventstradesmulticast A/Bperiodicacks / fillsBroker trading systemsOrder gateways (FIX)SequencerMatching engine(primary)Pre-trade risk checksHot standby engineDrop copy / trade feedMarket data publisherRetransmit & snapshotsvcReplicated journalBook snapshotsClearing corporationData vendors / members
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Aggressive limit buy that partially fills

  1. 1Broker sends NewOrderSingle: BUY 1,000 RELIANCE @ ₹2,900.00 limit, day order.
  2. 2Gateway decodes it, checks the session and message rate, and validates tick size, lot size and price band.
  3. 3Pre-trade risk checks the member's order value limits and kill-switch status, then forwards the order.
  4. 4Sequencer stamps global_seq = 88,412,907 and the timestamp, appends it to the journal, waits for the standby to confirm it has the bytes, then releases it to the engine.
  5. 5Engine looks at the best ask: 600 @ ₹2,899.50 (two orders, 400 then 200 by time). It fills 400 then 200, creating two trades at ₹2,899.50, the resting order's price.
  6. 6Next ask level is ₹2,900.50, which is above the limit, so matching stops. The remaining 400 rests on the bid side at ₹2,900.00, at the back of that level's queue.
  7. 7Engine emits: two trade events, one book update per changed level, and execution reports for the aggressor (two partial fills) and both passive orders (fills).
  8. 8Gateways send execution reports to each owning broker; the market data publisher multicasts the trades and book changes with feed sequence numbers.
  9. 9Drop copy sends the trades to the clearing corporation for settlement and to members' risk systems.

Primary engine fails

  1. 1The standby engine has been consuming the same sequenced stream and holds identical books, but its outputs are suppressed.
  2. 2Heartbeats from the primary stop; a failover controller (or an operator for exchange-wide events) confirms the primary is fenced off so it cannot keep sending.
  3. 3Standby checks it has applied every journal entry up to the last replicated sequence number, then switches its outputs on.
  4. 4Gateways and the market data publisher drop outbound messages they have already sent by (engine output sequence number), so no duplicate execution reports or ticks go out.
  5. 5Trading continues; a new standby is rebuilt from the latest snapshot plus journal replay.

7 Deep dives

Why single-threaded matching per symbol

Matching on one symbol is inherently sequential: whether order B trades depends on whether order A just took the liquidity. Multithreading one book needs locks, and locks add latency jitter and make the result depend on thread timing, which breaks determinism and fairness. So each symbol belongs to exactly one engine thread, pinned to a CPU core, with no locks and data that stays in L1/L2 cache. Scale out by partitioning symbols across threads and machines; symbols never interact in matching, so this is embarrassingly parallel. Balance partitions by load, not by count: index options and a few large-cap stocks get their own cores. Inside the thread, avoid anything that can pause: no garbage collection in the hot path (preallocated object pools, or C++/Rust), no system calls, no logging to disk from the thread. Communication with the sequencer and publisher uses lock-free single-producer single-consumer ring buffers, the pattern popularised by the LMAX Disruptor. A single thread can process millions of simple order events per second, so one core per busy symbol is plenty. The hard limit is not CPU but the cost of journaling and replicating each message before matching, which is why that path gets the most engineering.

Sequencer, journal and deterministic replay

The engine is a state machine: state_n+1 = apply(state_n, message_n). If every replica sees the same messages in the same order and apply() is deterministic, every replica has the same book. That is the whole fault tolerance story. The sequencer assigns a monotonically increasing sequence number to each inbound message and writes it to an append-only journal. Before the primary engine acts on message n, the journal entry must be durable on at least the standby (and ideally replicated to the DR site, asynchronously because of distance). This is why the primary never loses a trade it has published: anything it acted on is in the journal. Determinism rules: no wall-clock reads inside apply() (use the timestamp stamped by the sequencer), no random numbers, no hash-map iteration order dependence, no floating point for prices. Reference data changes, halts and end-of-session events are also sequenced commands. Recovery = load latest snapshot + replay journal from its sequence number. A replay of an hour of messages takes seconds because matching is fast. The same journal also feeds audit and surveillance: regulators can reconstruct the exact book at any microsecond.

Order book data structure

Operations to support: add an order, cancel by id, modify, and match against the best opposite price. Prices are integers in ticks. Because exchanges set a daily price band (say ±10-20%), the number of possible prices is bounded, so a fixed array of price levels indexed by (price - band_low) / tick_size gives O(1) access to any level. Keep a pointer to the best bid and best ask; when a level empties, scan to the next non-empty level, which is fast because the book is dense near the top. Without bands, use a red-black tree or skip list of levels (O(log n)). Each level is a FIFO doubly linked list of orders plus a cached total quantity, so adding at the tail and removing any order are O(1). A hash map from order_id to its list node makes cancel O(1), which matters because most messages are cancels and modifies. Modify semantics are part of fairness: reducing quantity keeps time priority; increasing quantity or changing price is treated as cancel-and-replace and goes to the back of the queue. Market orders walk levels until filled or the book is empty; IOC orders cancel any remainder instead of resting. Stop orders sit in a separate trigger book keyed by stop price and enter the main book when the last trade price crosses them.

Fair and recoverable market data

Every book change is published on UDP multicast, because one packet reaches thousands of subscribers at the same time and TCP fan-out would give earlier connections an advantage. Messages carry a per-channel sequence number. Two identical streams (A and B) go over separate network paths; receivers take whichever copy arrives first and use the other to fill gaps. If both miss a packet, the receiver asks a TCP retransmission service for that sequence range, or, after a long gap, requests a full snapshot and then applies incremental updates after the snapshot's sequence. Fairness extends to co-location: exchanges equalise cable lengths to member racks so no member gets data nanoseconds earlier because of location; NSE's 2015 tick-by-tick co-location issue in India showed how much regulators care about this. Publish tick-by-tick order-level data to co-located members and conflated snapshots (for example top-5 depth every second) for retail and vendors. The publisher runs off the engine's output stream, never in the matching thread, so a slow network does not slow matching.

8 Trade-offs

Sequencer before risk vs after risk

Putting risk checks before the sequencer keeps rejects out of the journal and the engine, but risk state (member exposure) then changes outside the deterministic core. Putting member-level risk inside the engine makes it deterministic but adds work to the hot path. Common choice: stateless validation and simple limits in gateways, exposure-based limits in a dedicated risk stage that is itself sequenced.

Synchronous replication to standby vs async

Waiting for the standby to persist each message adds a few to 20 µs per message, but guarantees no trade can be lost on failover. Async replication is faster but a primary crash can lose trades that were already reported to brokers, which is unacceptable. Replication to a distant DR site stays async, and DR failover is a declared event with reconciliation.

Continuous matching vs frequent batch auctions

Continuous price-time matching rewards speed and drives the latency arms race. Frequent batch auctions (match every few milliseconds at one clearing price) reduce the value of being microseconds faster. Most exchanges use continuous matching during the day and call auctions at open (and sometimes close). Mention the trade-off; implement continuous plus a pre-open auction.

Kernel bypass networking vs standard sockets

Kernel bypass (DPDK, Solarflare/OpenOnload, RDMA) and busy-polling cores cut network latency from tens of microseconds to a few. It costs dedicated hardware, CPU cores spinning at 100%, and harder operations. For a real exchange it is standard; for an interview design, say where it matters (gateways, sequencer, publisher) and that the matching logic itself is not the bottleneck.

9 Bottlenecks

One extremely hot symbol

On expiry day, at-the-money index options can take a large share of all messages. One symbol cannot be split across threads, so give it a dedicated core and machine, keep its gateway and journal path short, and throttle members per session. If needed, move other symbols off that partition during the day using a sequenced 'migrate' command at a quiet moment.

Journal write and replication

At 10x messages, the journal (not matching) saturates first. Batch journal writes (group commit every few microseconds), use NVMe with direct I/O or replicate over RDMA, and partition the sequencer by symbol group so there are several independent journals. Global order across partitions is not needed because symbols do not interact.

Order-to-trade ratio from algorithms

Algos may send 100 messages per trade, and most are cancels. Exchanges control this with per-session message rate limits and order-to-trade ratio charges. Technically, O(1) cancel and fast reject paths in the gateway keep this load cheap.

10 Failure handling

Primary engine crash

The hot standby has the same state from the same sequenced stream. After fencing the primary, the standby turns on its outputs. Gateways deduplicate outbound messages by engine output sequence, so brokers see each execution report once. Recovery time is seconds.

Sequencer failure

The sequencer is the most critical single point. Run a standby sequencer that receives the journal; on failover it resumes numbering from the last durably replicated sequence. Messages received but not journaled are lost from the exchange's view, and gateways tell brokers to resend or treat those orders as never received (brokers check status by client_order_id).

Site-wide outage

Trading halts. The DR site has an asynchronously replicated journal; operators compare the last sequence with member drop copies, decide the recovery point, and announce it before resuming. Exchanges rehearse this with mandatory live DR drills. Honest answer: an exchange prefers a short halt over trading on uncertain state.

11 What impresses

  • Explaining that determinism is the fault-tolerance mechanism: same inputs in the same order give the same state
  • Pointing out that journaling and replication, not matching logic, dominate the latency budget
  • Using a fixed array of price levels because of daily price bands
  • Explaining modify semantics: quantity decrease keeps priority, price change loses it
  • Discussing fairness: multicast, equal cable lengths, and why TCP fan-out is unfair
  • Mentioning mechanical sympathy: core pinning, cache locality, no GC, lock-free ring buffers

12 Mistakes

  • Using a database transaction per order or Kafka in the matching hot path
  • Multithreading a single order book with locks
  • Using floating point for prices
  • Reading the wall clock or random values inside the engine, breaking replay
  • Forgetting cancels and modifies, which are the majority of messages
  • Ignoring market data distribution and recovery from gaps

13 Follow-ups

Q1How would you implement the pre-open call auction to find the equilibrium price?
Q2How do you handle a member whose software goes rogue and floods orders (kill switch)?
Q3How would you test that the engine is deterministic?
Q4How would you add iceberg orders that show only part of their quantity?
Q5What changes if you must support cross-symbol orders like spreads?
Q6How would you prove to a regulator what the book looked like at 10:31:02.123456?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free