The whole design, in writing
Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.
The big idea
What is a stock exchange?
Buyers and sellers submit orders; the exchange matches them into trades — fairly, deterministically, and in microseconds. Money is on the line, so it must never lose an order, never mismatch, and treat every participant equally. Correctness and latency, both absolute.
At the center is a matching engine running a single, ordered stream of events against an in-memory order book. Wrap it in admission, risk checks, deterministic sequencing, market-data broadcast, and a durable journal — keeping the core path tiny and fast.
What the new pieces do
- Traderclient
- Sends buy/sell orders and expects acknowledgement and fills in microseconds. Latency and fairness are the product — a slow exchange loses everyone.
Step 1 · The core
Match orders into trades
A trader sends “buy 100 at $50”. The exchange must instantly find a matching sell and execute, or rest the order until one appears. What structure makes that match fast and correct?
"Buy 100 at $50" must instantly find a match or wait for one. What structure makes that fast and correct?
Scanning every open order per incoming order is O(N) on the hottest path in the system — far too slow for microsecond matching. You need a structure sorted for instant best-price lookup.
A round-trip to disk-backed storage per order is orders of magnitude too slow, and SQL can’t express price-time matching cheaply. The book must live in memory, purpose-built.
Best bid and best ask sit at the top; matching crosses the spread — highest bid against lowest ask — until they no longer overlap. A market order takes best price; a limit order rests if it can’t match.
An Order Gateway admits the order to a Matching Engine that runs against an Order Book — resting bids and asks per symbol. A market order takes the best price; a limit order matches if possible, otherwise rests in the book.
Why this piece earns its place
The structure above answers the arrival of a new order, but most of what actually hits an exchange is not a new order — it is a cancel or an amend, sent by market makers re-quoting as the price moves. If the book is only sorted queues of resting orders, a cancel has to walk the queue at that price level to find its order, so the one path that exists to relieve pressure becomes the slowest thing in the system exactly when quoting is heaviest. The book is therefore two structures kept in step: the price levels with their queues, and an order id → queue node index that lets a cancel unlink in constant time. The second thing this step hides is memory. Resting limit orders priced far from where trading is happening never match and never leave on their own, so the book grows through the session with orders that contribute nothing to any fill. Size the structure for the depth resting at the close, not for the rate at which trades occur — they are very different numbers.
What the new pieces do
- Order Gatewaybackend
- Authenticates the trader, validates the order, and admits it into the system. The boundary between the messy outside world and the pristine matching core.
- Matching Engineservice
- The heart: matches incoming orders against resting ones by price, then time. Kept single-threaded and in-memory so it’s deterministic and blazingly fast.
- Order Bookstore
- The live in-memory book of resting buy (bid) and sell (ask) orders per symbol, sorted for instant best-price lookup and matching.
Step 2 · The rules of matching
Price-time priority
When many orders want the same price, who trades first? Get this wrong and the market is unfair — and traders will (rightly) leave an exchange that doesn’t treat equal orders equally.
Many orders want the same price. Who trades first, fairly?
Favoring size lets big players jump the queue and punishes small orders — manifestly unfair, and traders will leave. Fairness can’t depend on order size.
Best price wins; among equal prices the earliest order fills first. Organizing the book as price levels, each a FIFO queue, builds the rule into the data structure — deterministic and auditable.
Randomness makes fills unpredictable and unauditable — a trader can’t reason about where they stand in line. Matching must be deterministic, so equal-priced orders need a stable order: time.
Match by price-time priority: best price wins, and among equal prices the earliest order fills first. The book is organized into price levels, each a FIFO queue, so the rule is built into the data structure.
Why this piece earns its place
Price-time priority is a promise about what can and cannot move a trader in the queue, and every feature added after this step is a chance to break that promise quietly. The moment order modification is on the table the rule needs an explicit answer: increasing size, or changing price, has to send the order to the back of its new level — otherwise a trader holds a spot at the front and keeps re-using it, which is queue-jumping with extra steps. Only reducing size can keep priority, because it takes nothing from anyone behind. The same question returns for hidden or iceberg size, for self-match prevention, and for any order type that gets to skip a queue. My rule is that each of those has to state its effect on priority in the same place the matching rule lives — in the book — rather than as a branch inside the matcher. That is what keeps the fairness claim auditable: a trader can reason about a rule encoded in the data structure, and cannot reason about an accumulated pile of exceptions to it.
- pricethen time
- FIFOper level
- deterministicfills
Back of the envelope
- sort by price, then time (FIFO)
- the matching rule is the data structure
- price levels = FIFO queues
- best bid / ask sit at the top
- deterministic, auditable fills
- every trader knows their place in line
Step 3 · Guard the book
Pre-trade risk checks
A trader could submit an order they can’t afford, or one that blows past position limits — a “fat finger” or a rogue algo. Letting that reach the book risks trades that can’t settle.
A trader could submit an order they can’t afford or that blows past limits (fat-finger, rogue algo). Where do you stop it?
Unwinding executed trades is disruptive, may be impossible once others traded against them, and corrupts the market. Bad orders must be stopped before they execute, not after.
Putting credit/position checks in the single-threaded hot loop adds latency and non-determinism to the core. Keep the matcher tiny and fast; validate before it.
Verify buying power, position/credit limits and price/size sanity at the gateway, rejecting violations immediately — so only valid orders ever touch the engine, and the hot loop stays fast and deterministic.
Insert a Risk Check before matching: verify buying power, position and credit limits, and sanity-check price/size. Reject violations immediately, so only valid orders ever touch the matching engine.
Why this piece earns its place
A risk check that reads position and buying power has a lag the diagram doesn’t show: the numbers it reads are updated by fills, and fills come back from the engine after admission. A trader firing orders faster than that fill stream returns can send a run of orders that are each individually inside the limit and collectively far outside it — which is precisely the shape of a runaway algorithm, so the check fails on the traffic it exists to stop. The fix is to stop treating admission as a read. Admission reserves: buying power is committed the moment the order is accepted, and released on cancel, on reject, and on whatever part of the order never traded. Now the limit holds no matter how far behind the fill stream runs, because the money and the order are committed in the same instant. What that costs is real and worth saying out loud: a trader’s usable capital is tied up by resting orders they may never trade, so quoting on both sides of a wide book consumes limit they are not really using. That complaint is the right one to accept here.
What the new pieces do
- Risk Checkguard
- Pre-trade gate: does the trader have the buying power and stay within position/credit limits? Rejects bad orders before they reach the book.
Step 4 · Make it deterministic
A single sequencer
Orders arrive concurrently from everywhere. For fairness, auditability, and the ability to run hot replicas, every component must agree on the exact order events were processed — and racing threads can’t guarantee that.
Orders arrive concurrently from everywhere. How do you make matching fair, auditable, and replica-safe?
Racing threads can’t guarantee the order events were processed, breaking fairness and making the engine non-reproducible — so replicas diverge. The core must process one agreed order.
A sequencer stamps every admitted order with a global sequence number, then a single thread processes that one stream — same input order, same output, on primary and every replica. Astonishingly fast in-memory, and perfectly reproducible.
Wall clocks differ across machines and tie at microsecond scale, so timestamps don’t give a single agreed order. You need one authoritative sequencer assigning a total order, not independent clocks.
Funnel all admitted orders through one Sequencer that assigns a global sequence number, then feed that single stream to a single-threaded matching engine. Same input order, same output, every time — on the primary and every replica.
Why this piece earns its place
Ordering the input is only half of determinism. The other half is a discipline inside the engine: it must never read anything that is not in the sequenced stream. A wall-clock read stamped onto a trade, a random tiebreak, an iteration over a hash map whose order depends on where things landed in memory, a background thread expiring day orders on its own schedule — every one of those is a place where the primary and a replica, fed identical input, quietly produce different books. Nothing reports it, and the disagreement surfaces at failover, over someone’s fill. So anything time-driven enters as an event through the sequencer and is processed at its sequence position like everything else; the engine asks the world nothing. The sequencer then deserves all the paranoia the matcher no longer needs, because it is the one component whose failure stops the market and whose own failover can corrupt it. A gap in the numbering is recoverable — consumers can detect and wait. A reissued number is not: two different events wear the same position, and every replica that trusts the stream diverges from every other.
- 1sequencer
- single-threadmatcher
- µsper match
What the new pieces do
- Sequencerservice
- Stamps every order with a single, global sequence number so all replicas process events in the exact same order — the basis of determinism and fairness.
Back of the envelope
- 1 sequencer ⇒ total order
- every replica sees the same input stream
- single-threaded, in-memory
- no shared-state races, µs per match
- same input ⇒ same output
- perfectly reproducible for failover
Step 5 · Tell the world
Market data feed
Every participant needs to see book updates and trades to make decisions — and they must all see them at the same time. Any latency advantage is an unfair edge worth millions.
Every participant needs book updates and trades — and a latency edge is worth millions. How do you distribute fairly?
Polling gives whoever polls fastest (or is closest) a systematic edge, and hammers the engine. Market data must be pushed equally, not pulled at each participant’s own rate.
Selling a latency advantage is exactly the unfairness a fair exchange must prevent — it lets some systematically see the market first. Delivery should be equal-latency for all.
The engine emits each change to a market feed (often multicast) so the same picture reaches everyone simultaneously. Distributing data fairly matters as much as matching fairly.
The engine emits every change to Market Data, broadcast over a Market Feed to all participants with equal-latency delivery (often multicast). The same picture reaches everyone simultaneously.
Why this piece earns its place
Broadcast buys fairness by giving up the thing point-to-point delivery hands you for free: the feed cannot slow down for anybody. If one subscriber falls behind and the publisher waits for it, that subscriber has just applied backpressure to the entire market — a worse unfairness than the one being solved. So the feed is deliberately fire-and-forget, and every message carries its own sequence number so a receiver can tell that it missed one rather than silently holding a corrupted book. Recovery then has to live off the broadcast path entirely: a separate retransmit-request channel with its own capacity, and a periodic full snapshot of the book so a client that starts late or gaps badly can resynchronise without ever asking the matching engine for anything. Running two identical feeds over independent network paths, with receivers taking whichever copy arrives first, turns most single-packet losses into a non-event. The snapshot cadence is the knob: too rare and a gapped participant trades blind for a long stretch, too frequent and the snapshots eat the bandwidth the live feed needs at the open, when gaps and volume both peak together.
What the new pieces do
- Market Datastore
- The stream of book changes and executed trades. Every participant needs the same view of the market at the same time.
- Market Feedbus
- Publishes market data to all participants simultaneously, with equal-latency delivery so no one gets an unfair early peek.
Step 6 · Never lose a trade
Journal & replay
The order book lives in memory for speed — but memory is volatile. A crash without a record would lose orders and trades, which for an exchange is catastrophic and possibly illegal.
The order book lives in volatile memory for speed. How do you survive a crash without losing trades?
A snapshot every few seconds loses every order and trade since the last one — catastrophic and possibly illegal for an exchange. Every event must be durable before it takes effect.
Log first, apply second: on failure a replica replays the journal to reconstruct the exact book, and because the engine is deterministic the replay reproduces an identical result. The sequential write is cheap.
Synchronously copying full book state on every change is slow and still loses the in-flight event on a crash. Journaling the ordered event stream is cheaper and, with determinism, rebuilds state perfectly anywhere.
Journal every event to an append-only log before applying it. On failure, a replica replays the journal to reconstruct the exact book state and takes over. Because the engine is deterministic, replay reproduces the identical result.
Why this piece earns its place
Log first, apply second settles durability. The ordering that actually bites is subtler: journal before you tell anyone. If a trade is published to the market feed, or acknowledged back to the trader, before that event is safely on the log, a crash in that window leaves the world holding a trade the replica will never replay. The rebuilt book is internally perfect and disagrees with what every participant already saw and traded against — which is strictly worse than losing the event outright, because nothing in the system detects it. So the journal write sits on the critical path ahead of every outbound message, and that makes what counts as written the real decision. A write into the operating system’s page cache is fast and survives the process dying, but not the machine losing power. A synchronous flush to the device survives the machine and charges latency to every event on the hot path. The usual resolution is to redefine durable as on the log on more than one machine, since simultaneous loss of several is far rarer than the loss of any one — which is also what puts the replica in a position to take over at all.
What the new pieces do
- Journalstore
- A durable, ordered log of every event before it’s applied. Replaying it rebuilds the exact book state — the safety net behind an in-memory engine.
Back of the envelope
- append before apply
- the sequential journal write is cheap
- deterministic ⇒ replay = rebuild
- the log is a perfect recipe
- replica replays ⇒ takes over
- in-memory speed without losing data
Step 7 · After the match
Clearing, settlement & scale
A match is a promise, not finished money. Ownership and cash must actually change hands, and one engine can’t hold the order books for every symbol on earth.
Hand executed trades to Clearing & Settlement (T+1/T+2) off the hot path, so post-trade work never slows matching. Scale by partitioning symbols across matching engines — each symbol’s book is independent, so AAPL and TSLA run on different engines in parallel.
Why this piece earns its place
Partitioning works because orders for different symbols never interact — and that assumption stops holding in two places worth naming before someone else names them. The first is the product. A spread or multi-leg order wants every leg to fill or none of them to, and if the legs sit on different engines nobody owns that atomicity. The answer isn’t cross-engine coordination, which would hand the hot loop a distributed transaction and cost it the determinism the last few steps bought; it’s to route a related family of products to one engine, and to treat a genuine combination as its own instrument with its own book that publishes into the outright books. The second is risk, and it’s the more awkward one. Buying power is per account, not per symbol, and one account trades across many engines — so the reservation taken on the admission path has to live in a single place that spans the whole partitioned fleet. That place is now in front of every order the exchange accepts. It is the honest scaling limit of this design: matching shards beautifully, and the thing standing in front of matching does not.
What the new pieces do
- Clearing & Settlementservice
- After a match, transfers ownership and money between parties (T+1/T+2). Decoupled from matching so settlement never slows the hot path.
You did it
You just designed a stock exchange.
Everything you assembled, in order
- A matching engine crossing an in-memory order book turns orders into trades.
- Price-time priority — best price, then FIFO — makes matching fair by construction.
- Pre-trade risk checks at the edge keep invalid orders out of the core.
- A single sequencer + single-threaded engine give deterministic, fair, fast matching.
- A market-data feed broadcasts the same view to all participants with equal latency.
- An append-only journal plus deterministic replay make the in-memory engine durable.
- Clearing runs off the hot path; partitioning by symbol scales the exchange out.