Engineering Atlas › System Design Interviews

Design a stock trading exchange

21 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

Requirements

Functional

Non-functional

Out of scope: clearing and settlement (a central counterparty takes over after the trade; US equities have settled on T+1, one business day after the trade, since 28 May 2024 (SEC)), opening and closing auctions in detail, the brokerage apps that retail investors use, and options and futures pricing.

Back-of-envelope

Planning numbers for a mid-to-large equities exchange.

API

Members connect over TCP to a gateway using a compact binary protocol (Nasdaq's OUCH is a public example); slower members use FIX through a translating gateway. Field names below are illustrative.

-> EnterOrder   { token: "A17", symbol: "ACME", side: B|S, qty: 500,
                  price: 10125 (in 1/100 cents) | MARKET, tif: DAY|IOC }
<- Accepted     { token: "A17", order_ref: 884201, seq: 73310021, ts_ns }
<- Executed     { token: "A17", qty: 200, price: 10120, match_id: 551903 }
<- Rejected     { token: "A17", reason: "price_band" | "risk_limit" | "halted" | "dup_token" }

-> CancelOrder  { token: "A17", qty_remaining: 0 }
<- Canceled     { token: "A17", qty_canceled: 300 }

-> ReplaceOrder { old_token: "A17", new_token: "A18", qty, price }   (loses time priority
                                                                    unless only qty decreases)
Market data (UDP multicast, gapless seq per channel):
   AddOrder { order_ref, side, qty, symbol, price }    OrderExecuted { order_ref, qty, match_id }
   OrderCancel { order_ref, qty }   OrderDelete { order_ref }   Trade { ... }
Recovery: UDP unicast re-request to a retransmission server { session, from_seq, count }; book snapshots over TCP.

The member's token is unique per session per day, so a resent order after a disconnect is recognised as a duplicate instead of becoming a second order. Prices are integers in a fixed tick unit; floating point never touches the book. The recovery path mirrors Nasdaq's MoldUDP64, where a listener that sees a gap sends a small request packet (session, first sequence number, message count) to a re-request server and gets the missing packets back by UDP unicast (MoldUDP64 spec).

Data model

In memory, per symbol (the order book). Two sides. Each side maps price to a level; each level holds a FIFO doubly linked list of resting orders and a running total quantity. Equities trade on a fixed tick grid inside a daily price band, so a level index can be an array indexed by (price - band_low) / tick, with best bid and best ask tracked as indexes. A hash map from order_ref to the order node makes cancel O(1). An order node is about 64 bytes, one cache line: ref, member, remaining quantity, price, timestamp, and the two list pointers.

Input journal. Append-only, one per matching partition, keyed by sequence number. Each record is the message as the sequencer stamped it. This is the system of record; everything else can be rebuilt from it.

Output journal. Execution reports and market data events, also sequenced, so a gateway or a market data subscriber can ask for "everything after seq N".

Session state per member connection (next expected sequence, tokens seen today, risk counters) lives with the gateway and is rebuilt from the journals on restart.

Reference data, loaded before the open: symbol to partition, tick size, price bands, member permissions. Symbol is the partition key because matching never crosses symbols. Trades then flow to an ordinary database for clearing and surveillance, off the critical path.

High-level design

flowchart LR
  M["Member trading systems"] --> G["Order gateways (session, validation, pre-trade risk)"]
  G --> S["Sequencer (cluster leader)"]
  S --> J[("Replicated input journal")]
  S --> E["Matching engine, one thread per partition"]
  J --> F["Follower engines (hot standby)"]
  E --> O["Output sequencer"]
  O --> G
  O --> MD["Market data publisher"]
  MD --> UDP["Multicast feeds A and B"]
  UDP --> M
  O --> DC["Drop copy, clearing, surveillance"]

Write path (an order that trades).

  1. A member sends EnterOrder to its gateway. The gateway checks the session, the token, the symbol's state and the price band, and runs pre-trade risk (order size and value limits, open exposure) from counters it keeps in memory.
  2. The gateway forwards the message to the sequencer for that symbol's partition, which stamps a sequence number and a timestamp and appends it to the journal. The journal entry is replicated to followers.
  3. Once a quorum has the entry, the matching thread processes it: it walks the opposite side from the best price while the order crosses, generating fills at each resting order's price, and rests any remaining quantity at the back of its price level's queue.
  4. The engine emits Accepted, Executed for both sides, and public events (OrderExecuted, AddOrder for the remainder) into the output sequencer.
  5. The gateway delivers execution reports to both members; the publisher multicasts the public events; drop copy and clearing receive the trades.

Read path (market data).

  1. Subscribers join the A and B multicast groups for their channels and take whichever copy of each packet arrives first.
  2. On a gap in sequence numbers, a subscriber requests the missing range from a retransmission server, or, if it is far behind, loads the latest book snapshot and replays from the snapshot's sequence.

Deep dives

1. The matching engine: one thread, one book

Options considered: a database with a row per order and transactions per match, a multithreaded engine with locks per price level, or a single thread per partition.

A database transaction per match costs milliseconds, three orders of magnitude over budget. Locked multithreading fails because every order touches the top of the book, the same few cache lines, so threads serialize anyway and pay for cache-line transfers between cores; worse, results now depend on thread scheduling.

I choose one thread per partition: read the next sequenced message, mutate the book in memory, write outputs, with no locks and no system calls. Martin Fowler's write-up of LMAX describes a single-threaded business logic processor handling about 6 million orders per second on a 2010-era dual-socket server, entirely in memory with no database on the hot path (Martin Fowler). Parallelism comes from partitioning: 8,000 symbols over, say, 32 engine threads, with the busiest symbols on threads of their own.

Inside the thread, the goal is to touch little memory. The best level comes from a tracked index, the level's FIFO gives time priority for free, and a fill against the first resting order touches about four cache lines. A replace that only lowers quantity keeps its queue position; any other change is a cancel plus a new order at the back.

2. Sequencing, determinism and failover

The options for high availability are a primary that writes state to a replicated database, active-active engines that each accept orders, or a replicated input log with deterministic replicas.

State replication is too slow for a book that changes millions of times a second, and active-active engines that order messages independently will disagree about who arrived first, which is fatal for an exchange.

I choose the replicated log. The sequencer is the cluster leader; it stamps each message and replicates it to two or more followers with a consensus protocol such as Raft (Aeron Cluster is an open-source Raft implementation that sequences many client connections into one replicated log and lists central limit order books among its typical uses (Aeron docs)). Every follower runs the same engine on the same entries and holds an identical book, provided the engine is deterministic: time comes only from the sequencer's stamp, no unordered hash-map iteration, no floating point, no dependence on thread timing.

Failover is a leader election: a follower with every committed entry takes over with a book that is already correct. Gateways resend unacknowledged orders and the token deduplicates them. Snapshots written by a follower, so the leader never pauses, bound restart to "load snapshot, replay a few minutes of log". Even the coarse version is fast: LMAX snapshotted nightly and reported a full restart, including replaying a day's journal, in under a minute (Martin Fowler).

The sequence number, not any timestamp a member claims, decides who was first. Exchanges also equalize cable lengths from every colocation cage, and IEX went further with a "speed bump" that delays inbound orders and messages by 350 µs while its own view of other venues' prices is not delayed. The coil was famously 38 miles of fiber; after IEX moved its primary data center in 2024 it lengthened the coil to 44 miles to keep the delay at 350 µs (IEX). The delay is the product, not the cable.

3. Market data and pre-trade risk at speed

Every book change goes to thousands of subscribers. TCP per subscriber would make the publisher's send order an unfair advantage and multiply bandwidth. UDP multicast sends each packet once and lets switches copy it, so every colocated subscriber gets the same bytes at nearly the same time. Reliability is added back by the protocol: gapless sequence numbers per channel (Nasdaq frames ITCH messages in MoldUDP64 packets, and each packet is transmitted once however many listeners there are (MoldUDP64 spec)), two identical feeds on separate paths, and a retransmission server plus snapshots.

Pre-trade risk sits on the gateway so it runs in parallel across members: per-order limits (quantity, notional, price band) and per-member exposure from counters fed by execution reports, with a margin because the counters lag the engine by microseconds. The kill switch makes the gateway reject everything from a member and injects mass-cancels into the sequenced stream, so they are ordered like any other message.

The same problem shape, one ordered log driving identical replicas, appears in Design Google Docs with operations on a document and in Raft itself, which is a replicated log with a deterministic state machine on top.

Down the stack

Layer 1The contract

  • Acknowledged means durable. Accepted or Executed is sent only after the input that caused it is on a quorum of journal replicas.
  • Priority. For any two resting orders on the same side, the one at the better price fills first; at the same price, the one with the lower sequence number fills first.
  • Determinism. Output for input sequence numbers 1 to n is a pure function of those inputs and the start-of-day reference data.
  • Market data. Within a channel, events carry consecutive sequence numbers; a subscriber that applies them in order holds the exact public book.
  • Idempotent entry. A second EnterOrder with a token already used that day in that session is rejected as a duplicate, never matched.

Layer 2The mechanism

The core is an event loop over a ring buffer, the structure the LMAX Disruptor made famous (Disruptor paper). The network thread writes sequenced messages into pre-allocated slots of a power-of-two ring; the matching, journal and replication threads each read the slots in order and publish their own sequence counter, which producers check before overwriting a slot. No queue nodes are allocated and no locks are taken.

Per message, the engine validates against book state (halted? order ref known?), matches while crossing, rests or discards the remainder by time-in-force, and emits events. Market data mirrors book operations one to one: ITCH's A (add), E (executed), X (partial cancel), D (delete) and U (replace) let a subscriber keep a full-depth copy of the book, and every message carries a timestamp in nanoseconds since midnight (Nasdaq TotalView-ITCH 5.0).

Layer 3The machine

  • Network. The kernel network path costs a few microseconds per packet in syscalls, copies and interrupts. Kernel-bypass stacks (DPDK, or Solarflare's OpenOnload and ef_vi) poll the NIC's receive ring from user space and cut that to around a microsecond. NIC hardware timestamps synchronized with PTP give nanosecond-resolution audit records.
  • CPU. The matching thread is pinned to an isolated core (isolcpus, interrupts steered elsewhere) and busy-spins, because waking a sleeping thread costs tens of microseconds. An L1 hit is about 1 ns and DRAM about 80 to 100 ns, so order nodes are one 64-byte cache line and hot books stay in cache.
  • Memory. Order nodes come from pre-allocated pools, so the steady state allocates nothing: no garbage collection pauses on the JVM, no malloc contention in C++ or Rust.
  • Storage. Durability comes from replication into other hosts' memory (10 to 30 µs round trips in a rack), with the journal written to disk in large sequential appends, not an fsync per message.
  • Physics. Light in fiber travels about 5 ns per metre, so 100 m of extra cable costs 0.5 µs.

Why it's built this way

  1. Members compete on who is first, so the exchange must define one total order of events; that order is the sequencer's log.
  2. A total order processed by many threads would need locks on the same top-of-book data, so one thread per book is both faster and simpler.
  3. One thread per book is a single point of failure, so availability comes from replicas running the same deterministic code on the same log.
  4. Replicas only agree if the code is deterministic, which forbids wall clocks, unordered iteration and floating point in the engine.
  5. Microsecond budgets make every kernel crossing and cache miss visible, so the machine is tuned around the one hot thread: bypassed network, pinned core, pre-allocated memory.

Failure modes

In the wild

Trade-offs

OptionStrengthsWeaknessesChoose when
Single thread per bookNo locks, deterministic, cache friendlyHot symbol capped by one coreMatching engines, almost always
Locked multithreaded bookUses more cores per symbolContention on top of book, nondeterministicRarely, if ever
Replicate input log, deterministic replicasFast failover, perfect audit replayEngine must be strictly deterministicExchanges and other order-sensitive state machines
Replicate state to a databaseFamiliar toolingToo slow for millions of changes per secondPost-trade systems, not matching
Multicast market dataFair, one send for all subscribersNeeds gap recovery and redundant feedsColocated, latency-sensitive subscribers
TCP or WebSocket market dataReliable, internet friendlyPer-subscriber cost, ordering unfairnessRetail and remote subscribers, via a separate distribution tier
Continuous matchingImmediate executionRewards raw speedMost of the trading day
Frequent batch auctionsRemoves speed races within a batchDelay, unfamiliar to membersOpens, closes and some alternative venues

Leadership lens

Check yourself

  1. Why use one thread per order book instead of a multithreaded engine with locks per price level?

    Show answer

    Every order touches the top of the book, the same few cache lines, so locked threads serialize anyway, pay for cache-line transfers between cores, and produce results that depend on thread scheduling. One thread per partition needs no locks or system calls and is deterministic; scale comes from partitioning symbols across engines.

  2. Why replicate the sequenced input log rather than the book state or run active-active engines?

    Show answer

    State replication is too slow for a book that changes millions of times a second, and active-active engines would disagree about who arrived first. Deterministic followers replaying the same log hold an identical book, so failover is a leader election in milliseconds to low seconds.

  3. The engine leader crashes right after receiving an order. Is anything lost or duplicated?

    Show answer

    No acknowledged order is lost, because Accepted is sent only after the input is on a quorum of journal replicas. Members resend unacknowledged orders with the same tokens, which deduplicate them, and market data continues from the next sequence number, so subscribers see a pause, not a gap.

  4. Why can all order books be held in memory with room to spare, and why does it matter?

    Show answer

    With 8,000 symbols, about 5,000 resting orders each and 64-byte order nodes, the total is about 2.6 GB. A busy symbol's book fits in a core's L2 or L3 cache, which is what makes microsecond matching possible.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →