The 100 problems, solved / Commerce, booking and payments / 049 · Design a stock trading system with a limit order book

Design a stock trading system with a limit order book

Match orders in microseconds with a strict price-time priority and a durable trail.

Concepts this design leans on

01The design, in the six-step framework

1. Requirements

Functional

  • Accept limit and market orders to buy or sell a symbol; cancel open orders.
  • Match orders by price, then time; partial fills allowed.
  • Publish the order book (top levels) and every trade to subscribers.
  • Keep an exact, replayable record of every order event for clearing and regulators.
  • Out of scope: clearing and settlement, margin, market data pricing for vendors.

Non-functional

  • 10,000 symbols; 1 million orders a second at peak across the exchange; a hot symbol 50,000 a second.
  • Order acknowledged and matched in under 100 microseconds inside the matching engine; end to end under 1 ms in the data centre.
  • Deterministic: the same input sequence always produces the same fills; strict price-time priority.
  • No lost or duplicated order; failover of the matching engine in under 1 s with no state loss.
  • Fairness: order of arrival at the gateway is the order of matching.
Back of the envelope

1 M orders/s × 100 B = 100 MB/s into the sequencer, 8.6 TB a day of order events; stored as an append-only log and archived.

A single-threaded matching engine does a few million operations a second in memory; the hot symbol at 50,000 a second uses a few percent of one core, so one engine can host many symbols, and 10,000 symbols spread over a few dozen engines by symbol.

The order book for one symbol is a few thousand resting orders; 10,000 symbols is tens of millions of orders in memory, a few gigabytes.

Market data: every book change is an event; 1 M a second of small messages to be multicast to subscribers, which is why it goes out as a sequenced feed rather than per-client messages.

2. Core entities

  • Order: order_id, client_order_id, account, symbol, side, type, price, quantity, remaining, state, sequence.
  • Order book: symbol, bids and asks as price levels, each a FIFO of resting orders.
  • Trade: trade_id, symbol, price, quantity, buy order, sell order, sequence, matched_at.
  • Event: sequence number, type (new, cancel, fill, reject), payload; the log of truth.

3. API

  • POST /orders with client_order_id, symbol, side, type, price, quantity; returns order_id and ack. In practice a binary protocol over a persistent session, since HTTP is too slow.
  • DELETE /orders/{id}: cancel; returns cancelled or already filled.
  • GET /orders/{id}: state and fills.
  • SUB market-data/{symbol}: a sequenced stream of book updates and trades.
  • GET /events?from_seq=…: replay for recovery and audit.

4. High-level design

ClientsEdgeEngineDataordersappendsequencedbook changesfeedreplayacks, fillsTrading clientsOrder gateways(validate, risk)Sequencer(single total order)Matching engines(by symbol, in memory)Market data publisherEvent log(replicated, append-only)Standby engines(replaying)Archive + clearing feed

One limit order:

  1. The client's session sends the order to a gateway, which validates fields and pre-trade risk (account limits, price bands) in microseconds.
  2. The gateway forwards to the sequencer, which assigns the next global sequence number, appends the event to the replicated log, and forwards it to the matching engine owning the symbol.
  3. The engine applies the order against the book: for a buy, it walks the ask levels from the lowest price while the price allows, filling in FIFO order at each level; any remainder rests on the bid side at its price.
  4. Fills and the ack are emitted as events (also sequenced), sent back to the gateways for the clients involved, and to the market data publisher, which multicasts the book change to subscribers.
  5. Standby engines consume the same log and maintain identical books; the archive consumer writes events for clearing and audit.

5. Deep dives

How does the book answer in microseconds, and what data structure is it?

A generic sorted map in a database is milliseconds; the book lives in memory with price levels in a sorted structure (an array indexed by price tick for liquid symbols, a tree for sparse ones) and each level holding a FIFO list of orders; a hash map from order_id to its node makes cancels O(1). Matching is a walk from the best level, so a marketable order touching three levels is a few dozen pointer operations. The engine is single-threaded per symbol, allocates nothing on the hot path, and never blocks on I/O: durability is the sequencer's job, done before the engine sees the event.

Why a sequencer, and who decides the order of two orders arriving at the same microsecond?

OptionBehaviourTrade-off
Timestamps from gatewaysEach gateway stamps; the engine sortsGateway clocks disagree; ties are arbitrary; not reproducible
Engine-local arrival orderEach engine orders what it receivesFine per symbol, but there is no single log to replay for the whole exchange
A single sequencer assigning a global sequenceOne total order for everything; the log is the truth; engines are deterministic functions of itOne component on the critical path; must be fast and replicated

The sequencer is the standard answer: it turns the exchange into a deterministic state machine over an ordered log, which gives ordering, replay and failover in one stroke. Fairness is defined as sequence order, which is arrival order at the sequencer, and that definition is published.

How does failover lose nothing and change nothing?

Because engines are deterministic consumers of the log, a standby engine that has replayed every event has exactly the same book. When the primary stops heartbeating, the standby is promoted within a second and continues from the next sequence number; no state is transferred. The sequencer itself is replicated as a leader with followers that acknowledge each append before it is forwarded, so the log never has a gap; its failover elects the follower with the highest sequence. Orders in flight at the gateway during failover are held and re-submitted with their client_order_id, which the engine uses for deduplication.

How is market data fanned out to thousands of subscribers at a million updates a second?

Per-client sends would saturate the publisher. The publisher emits one sequenced multicast feed per symbol group; subscribers in the data centre receive it directly and detect gaps by sequence number, requesting replay from a recovery service. Remote subscribers receive a conflated feed (top levels at fixed intervals) over normal connections. The same sequence numbers appear in the feed and in the order log, so a subscriber can prove what it saw.

What about the write-heavy archive and the 8.6 TB a day?

The log is append-only on fast local disks with replication, and an archiver consumer copies closed segments to object storage within minutes; the write path never touches a general-purpose database. Clearing reads trades from the archive at end of day; regulators receive the full event log, which is the point of making the log the truth rather than a side effect.

6. What a strong answer sounds like

  • Say the shape immediately: sequencer, deterministic single-threaded engines per symbol, replicated log, standbys by replay.
  • Describe the book's data structures and why cancels and matches are constant or logarithmic time.
  • Define fairness as sequence order and explain why timestamps cannot do the job.
  • Expect follow-ups on self-match prevention, on halts and auctions at the open, and on how you would test determinism (replay yesterday's log and compare).
  • A common mistake is to put orders in a database and match with queries; latency targets are a thousand times too tight for that.

02Where this question was reported