Design a Stock Exchange Matching Engine
This page is one interview loop in three rounds. All three rounds design the same system. Each round opens with the interviewer raising the scope, and the design from the round before has to evolve to meet it.
| Round 1: Mid-level | Round 2: Senior | Round 3: Architect | |
|---|---|---|---|
| Story | A small regional exchange with 100 instruments | A national equities exchange: 10,000 symbols, bursts at the open and close | A regulated venue: audit, fairness, a disaster-recovery site, cloud or colocation |
| Level (Amazon) | SDE II (L5) | Senior SDE (L6) | Principal (L7) |
| Volume | 2M orders/day; ~1,000/s peak | 500M orders/day (21,367/s average); ~214K/s exchange-wide peak second; 200K/s microbursts on the busiest partition | Meme-stock days at 3×: 1.5B orders/day, ~641K/s peak second |
| State | ~64 MB of order books (worst case); 256 MB of journal a day | ~11 GB of order books with pools; 32 GB of journal a day | 32 to 96 GB of journal a day; ~37 TB of compressed audit archive at steady state |
| Footprint | 1 AWS Region, 3 AZs | 1 colocation site with a hot standby per engine | Primary site, a nearby journal bunker, a far disaster-recovery site, and an AWS Region |
| Targets | Deterministic matching; millisecond latency; no acknowledged order lost | Matching P99 < 50 µs (a target); RPO 0; failover in milliseconds | No released trade lost even if a whole site burns; documented RTO; bit-identical replay for regulators |
| Reading time | ~35 min | ~40 min | ~45 min |
You can start at any round. Rounds 2 and 3 open with a "Where we left off" summary that catches you up.
Loop Opener: What Is a Matching Engine?
An Auctioneer Who Never Sleeps
Picture an auction hall for one stock. Buyers shout the highest price they'll pay. Sellers shout the lowest price they'll accept. The auctioneer writes every shout on a board, and whenever a buyer's price meets a seller's price, the auctioneer bangs the hammer and a trade happens. When two buyers shout the same price, the one who shouted first goes first.
A matching engine is that auctioneer as software. It never gets tired, it never plays favorites, and it has to do the whole thing thousands of times a second.
A few words carry this whole loop. Each gets one line now:
| Term | One-line definition |
|---|---|
| Order | An instruction to buy or sell a quantity of one instrument (a stock, say) |
| Limit order | "Buy 100 at 100.05 or better": it trades only at that price or a better one, and whatever is left waits in the book |
| Market order | "Buy 100 at whatever price is available now": it takes the best prices on the other side until it's filled |
| Order book | The list of all waiting (resting) limit orders for one instrument, split into two sides |
| Bid / ask | A bid is a resting buy order; an ask (or offer) is a resting sell order |
| Best bid / best ask | The highest bid and the lowest ask; together they are the top of book |
| Spread | Best ask minus best bid. If it's zero or negative, something should have traded |
| Tick | The smallest allowed price step (one cent in this loop). We store prices as whole numbers of ticks |
| Price-time priority | Better price first; at the same price, earlier arrival first (first in, first out) |
| Fill / execution | One trade between an incoming order and one resting order |
| Market data | The public stream of what happened: orders added and removed, trades, and prices |
Synthesizing vector architecture diagram...
Orders flow in from both sides. Two kinds of news flow out: a private execution report to the firms involved in each trade, and public market data that everyone sees at the same moment. Trades also go to clearing, where the money and shares actually change hands later.
What Makes It Hard
- Fairness is the product. An exchange sells one thing: a fair, predictable place to trade. If two firms send the same order and the slower one gets filled first, the exchange has failed, however fast it is.
- The result must be reproducible. Given the same orders in the same sequence, the engine must produce exactly the same trades, today and when a regulator asks about this day years from now.
- It's fast and bursty. At the opening bell, thousands of orders arrive in a millisecond, and the busiest instrument can't be split across machines (you'll see why in step 1.2).
- Nothing acknowledged may be lost. If we told a firm "your order is in" or "you bought 800 shares", that must survive any single crash.
So every component in this loop defends one of three rules:
| Rule | In one line |
|---|---|
| One order of events | Every input gets one position in one sequence, and everyone is treated by that sequence. |
| Same inputs, same outputs | The engine is deterministic: replaying the sequence rebuilds the same book and the same trades, bit for bit. |
| Nothing released is lost | We never tell anyone about an order or a trade until the input that caused it is safely stored twice. |
The Question the Whole Loop Answers
How do we match orders fairly, deterministically and fast, and never lose one?
The answer gets sharper every round:
- Round 1: an in-memory order book, one thread per book, a sequencer, and a replicated journal we can replay.
- Round 2: make it microseconds and failure-proof: no allocation, lock-free handoff, a hot standby in lockstep, and market data that participants can recover.
- Round 3: the rules of a market: an audit trail regulators can replay, fairness by design, a disaster-recovery site that loses no trades, and where the machines should live.
The surprising lesson, which we'll earn step by step: the fastest design and the most correct design are the same one. One thread per order book, fed by a sequencer, with state rebuilt by replaying a journal.
Round 1 · Mid-level · "A Small Exchange for 100 Instruments"
~35 min · SDE II (L5) · 1 region, 3 AZs · ~1,000 orders/s peak · millisecond latency · no acknowledged order lost
R1.1 Establish Design Scope
The interviewer says: "We're launching a small regional exchange. Design the part that takes orders and matches them." Before we draw anything, we ask questions, and we say out loud what each answer changes.
| We ask | Interviewer answers | What it changes in the design |
|---|---|---|
| Which order types? | Limit and market orders, and cancel. Limit orders are good for the day (DAY) or immediate-or-cancel (IOC). | A small, closed set of inputs. DAY orders expire at the close, so every book is empty overnight. |
| What's the priority rule? | Price, then time. | The book must keep orders at each price in arrival order, and "arrival" must be defined precisely (step 1.3). |
| How many instruments, and how busy? | 100 instruments. About 2 million orders and cancels a day, busiest in the first and last minutes. | Small. We derive about 1,000 inputs a second at peak (R1.7). |
| How fast? | Milliseconds are fine. Our members are not high-frequency traders yet. | We can run in an AWS Region on ordinary instances. Microseconds come in Round 2. |
| What market data? | Trades, the top of book, and the full depth for anyone who wants it. | A public, sequenced output stream (step 1.5). |
| Pre-trade risk checks? | Yes, basic ones: a credit limit per member firm, and size checks against fat-finger errors. | A check before an order can reach the book (step 1.6). |
| Trading hours? | 09:30 to 16:00, weekdays. | 6.5 hours, or 23,400 seconds, a day. Maintenance happens outside those hours. |
Out of scope for this round:
- Microsecond latency, hot standby, circuit breakers. Round 2.
- Clearing and settlement. We hand trades to a clearing firm in a file; the clearing house moves money and shares.
- Auctions (opening and closing crosses). The session starts with an empty book and continuous trading.
- Order types beyond limit, market and cancel (stops, icebergs, pegs, modify). A modify is a cancel plus a new order for now.
Starting small is a real design choice, not a shortcut. Every order type is another path through the matching code that must stay deterministic. Exchanges add them one at a time, each with its own tests.
R1.2 Functional Requirements, Derived Step by Step
| Phrase from the problem | Requirement |
|---|---|
| "Takes orders" | Accept a new limit or market order from an authenticated member session; reject it with a reason if it's invalid or fails risk checks |
| "Matches them" | Match by price-time priority, at the resting order's price, and rest any unfilled limit quantity in the book |
| "The firm must know what happened" | Send an execution report for every acceptance, fill, cancel and rejection |
| "Members change their minds" | Cancel a resting order; say clearly if it was already filled ("too late to cancel") |
| "Everyone sees the market" | Publish trades, top of book and depth as a sequenced public stream |
| "Trades must be settled" | Record every trade for the clearing handoff |
Not yet: microsecond latency, a hot standby, price collars and halts. The interviewer may bring these back.
R1.3 Non-Functional Requirements: the Questions
Numbers come in R1.7. For now, the questions, in the order that matters for an exchange:
- Determinism and fairness. The same inputs in the same order must give the same trades. Two orders at the same price must be served in arrival order, and "arrival" must mean one thing everywhere.
- Durability of accepted orders. Once we acknowledge an order, a cancel or a trade, it must survive a crash of any one machine, and even the loss of an Availability Zone (AZ).
- Availability during trading hours. Outside trading hours we can do maintenance. During them, every minute down is a minute nobody can trade.
- Latency. Milliseconds, measured from the order reaching our front door to the execution report leaving it.
R1.4 The API
Real exchanges speak compact binary protocols (Round 2). A small venue can start with JSON over HTTPS for orders and a WebSocket for streams, and that's what we show first, because it makes the fields easy to read.
Submit an order. The member sends its own client order ID: a unique ID the firm generates for each new order. It doubles as an idempotency key: if the firm resends the same ID (after a timeout, say), we return the first result instead of entering a second order.
httpPOST /v1/orders HTTP/1.1 Host: orders.exchange.example.com Authorization: Bearer <member session token> Content-Type: application/json { "client_order_id": "GSX-20260927-000417", "instrument": "XYZ", "side": "BUY", "type": "LIMIT", "price_ticks": 10005, "quantity": 800, "time_in_force": "DAY" }
price_ticks is the price in ticks: with a one-cent tick, 10005 means 100.05. Prices are integers so that "equal price" is exact (a floating-point price like 100.05 can't be stored exactly in binary, and two prices that print the same might not compare equal).
httpHTTP/1.1 200 OK Content-Type: application/json { "order_id": "XYZ-5001", "client_order_id": "GSX-20260927-000417", "sequence": 5001, "status": "FILLED", "fills": [ { "trade_id": "T-5001-1", "price_ticks": 10004, "quantity": 200 }, { "trade_id": "T-5001-2", "price_ticks": 10004, "quantity": 400 }, { "trade_id": "T-5001-3", "price_ticks": 10005, "quantity": 200 } ], "leaves_quantity": 0 }
sequence is the order's position in the exchange's one sequence of events (step 1.3). leaves_quantity is what's still resting. The trade IDs are built from the sequence number and a fill counter, so a replay produces the same IDs.
Cancel an order:
httpDELETE /v1/orders/XYZ-4102 HTTP/1.1 Host: orders.exchange.example.com Authorization: Bearer <member session token>
It returns 200 with "status": "CANCELLED" and the cancelled quantity, or 409 with "reason": "TOO_LATE_TO_CANCEL" if the order was already filled. A cancel is itself an input with its own sequence number, so "did the cancel or the fill win?" has exactly one answer.
| Status | When |
|---|---|
200 OK | The engine processed the order: NEW (resting), PARTIALLY_FILLED, FILLED, or CANCELLED (an IOC or market order's unfilled remainder) |
200 OK, same body as before | A resend of a client_order_id we've already seen today: the stored result |
400 Bad Request | Unknown instrument, a price that isn't a whole number of ticks, zero quantity |
403 Forbidden + "reason": "CREDIT_LIMIT" | Rejected by pre-trade risk (step 1.6); never reached the book |
409 Conflict | Cancel of an order that is already filled or cancelled |
503 Service Unavailable | The engine is failing over (R1.9); nothing was entered, resend with the same client_order_id |
Streams (WebSocket): /v1/stream/executions sends the member's own execution reports (fills can arrive long after the order was entered). /v1/stream/marketdata?instrument=XYZ sends public market data. Every message carries a sequence number so the client can spot a gap (step 1.5).
Recap
- One order call with a client order ID that makes resends safe; one cancel call; two streams.
- Prices are integer ticks.
- Every input gets a sequence number, and so does every output.
- Correctness first: price-time priority, and nothing acknowledged may be lost.
Let's build it, starting with the simplest thing that works.
R1.5 Design Evolution: From a Database Table to a Journaled Engine
Every step below follows the same pattern: a problem, your turn to think, the answer, and what the answer costs us. The cost is always the next problem.
Step 1.0: The Baseline
Orders go into a SQL table. A matching service, on each new order, queries the best opposite order and updates both rows.
Synthesizing vector architecture diagram...
What's good about it: everyone understands it, and the database keeps the data safe.
What it costs us: every match is several round trips to the database, and three API instances run matches for the same instrument at the same time. The next six steps are the ways it breaks.
Step 1.1: Matching by SQL Is Slow and Racy
The problem: a busy instrument receives 100 orders a second at the open. Each match takes several queries (find the best opposite order, lock it, update it, maybe repeat for the next level), so matches queue behind each other's row locks, and P99 latency climbs to hundreds of milliseconds. Worse, two API instances sometimes fill the same resting order. What would you do?
The book, drawn. Instrument XYZ, one tick = 0.01. Numbers in the level boxes are the total quantity at that price.
Synthesizing vector architecture diagram...
Read each row left to right: a price level, then its orders from oldest to newest. The best bid is 100.02 and the best ask is 100.04, so the spread is two ticks. The dotted arrow is the index: a cancel of B2 jumps straight to its node.
The same book as a table, the way a trader's screen shows it:
| Bid orders (oldest first) | Bid qty | Price | Ask qty | Ask orders (oldest first) |
|---|---|---|---|---|
| 100.07 | 300 | S5 | ||
| 100.05 | 700 | S2 | ||
| 100.04 | 600 | S1 (200), S3 (400) | ||
| B1 (300), B4 (200) | 500 | 100.02 | ||
| B2 (500) | 500 | 100.01 | ||
| B3 (1,000) | 1,000 | 100.00 |
Matching, as pseudocode. The trade price is always the resting order's price: the incoming order agreed to pay up to its limit, and the resting order was there first at its own price.
texton_new_order(o): # o.side, o.type, o.price_ticks, o.qty, o.tif opp = asks if o.side == BUY else bids while o.qty > 0 and opp is not empty: level = opp.best() # lowest ask or highest bid if o.type == LIMIT and not crosses(o.price_ticks, level.price): break # the best opposite price is worse than our limit while o.qty > 0 and level.queue is not empty: r = level.queue.head # oldest resting order at this price q = min(o.qty, r.qty) emit TRADE(price = level.price, qty = q, maker = r.id, taker = o.id) o.qty = o.qty - q r.qty = r.qty - q level.total = level.total - q # every fill, partial or full if r.qty == 0: level.queue.unlink(r); index.remove(r.id); pool.release(r) if level.queue is empty: opp.remove_level(level) if o.qty > 0 and o.type == LIMIT and o.tif == DAY: rest(o) # append to its level's tail, add to index emit ACCEPTED(o.id, leaves = o.qty) else if o.qty > 0: emit CANCELLED(o.id, qty = o.qty) # IOC or market remainder never rests crosses(limit, resting_price): BUY: resting_price <= limit SELL: resting_price >= limit on_cancel(id): r = index.get(id) if r is missing: emit CANCEL_REJECT(id, TOO_LATE_TO_CANCEL); return level = r.level level.queue.unlink(r); level.total = level.total - r.qty index.remove(id); pool.release(r) if level.queue is empty: side_of(r).remove_level(level) emit CANCELLED(id, qty = r.qty)
One subtle bug catches many first versions: reducing the level's total only on partial fills, and counting on the unlink to handle full fills. By the time the unlink runs, the order's quantity is already zero, so the total stays too high, the empty level is never removed, and the "best price" points at a ghost level. The pseudocode reduces the total on every fill.
A worked example. Order B9 arrives: buy 800 at 100.05, DAY, sequence 5001.
- Best ask is 100.04, which is ≤ 100.05, so it crosses. The head is S1 (200): trade 200 at 100.04. S1 is done and unlinked. B9 has 600 left.
- Next in the queue is S3 (400): trade 400 at 100.04. S3 is done. The 100.04 level is empty and removed. B9 has 200 left.
- Best ask is now 100.05, still ≤ 100.05. The head is S2 (700): trade 200 at 100.05. S2 has 500 left and keeps its place at the head.
- B9 is filled. Nothing rests. Three trades, which are exactly the three fills in the API response above.
After B9, the best ask is 100.05 (500, S2) and the spread is three ticks. Then a cancel for B2 arrives: the index finds B2's node, the 100.01 level becomes empty and is removed, and the best bid is unchanged at 100.02.
| Operation | Cost | Why |
|---|---|---|
| Find best price | O(1) | Levels are kept sorted; the best is at the front |
| Add an order at an existing level | O(1) | Append to the tail of the level's queue |
| Add an order at a new price level | O(log L) for L levels in a tree; O(1) in a price-indexed array (Round 2) | Insert a level into the sorted structure |
| Cancel | O(1) | Index lookup, then unlink from the doubly-linked list |
| Match | O(number of fills) | Each fill removes or reduces the head order |
Step 1.2: Two Threads Matched Against the Same Resting Order
The problem: we have the book in memory, and the order API runs 8 worker threads. Two buy orders for XYZ arrive a microsecond apart on two threads. Both look at the head of the 100.04 level, both see S1 with 200 shares, and both fill it. S1 has now "sold" 400 shares it never had. What would you do?
We group the 100 instruments into 4 instrument groups of 25, each owned by one matching thread. Four is a choice: it's plenty for the load and keeps one slow instrument from delaying all 100.
Synthesizing vector architecture diagram...
Each book has exactly one writer. Threads never share a book, so there are no locks, and each thread sees its instruments' inputs in the same order every time.
Step 1.3: Which Order Came First?
The problem: orders arrive through three API gateways, one per AZ. Gateway A stamps an order at 10:00:00.001200 and gateway B stamps another at 10:00:00.001150, but B's clock runs 300 microseconds fast. Which order is first? A member complains that its order was stamped earlier but filled later. What would you do?
The sequenced input is the heart of the whole design. Everything that follows (the journal, replay, the standby, the audit trail) is just a copy of this one stream.
Step 1.4: The Engine Crashed. Where Is the Book?
The problem: the engine instance fails at 11:14. Its books were in memory. Members had open orders, some partly filled, and they were told about every fill. How do we bring back exactly the same books? What would you do?
What's in a journal record. Every input becomes one fixed-size binary record. In Round 1 we reserve 128 bytes per record, which is generous:
| Field | Type | Size | Why |
|---|---|---|---|
sequence | uint64 | 8 B | Position in the one sequence; gap-free |
epoch | uint32 | 4 B | Which engine incarnation wrote it (see below) |
timestamp_ns | uint64 | 8 B | Sequencer's clock, nanoseconds since midnight UTC; the only time the engine ever uses |
gateway_id, gateway_seq | uint16, uint64 | 10 B | Which gateway sent it and its number there, so resends are recognized |
member_id, session_id | uint32, uint32 | 8 B | Who sent it |
kind | uint8 | 1 B | NEW, CANCEL, TIMER, CONTROL |
| order fields | mixed | ~40 B | Instrument, side, type, time in force, price in ticks, quantity, client order ID |
crc32c | uint32 | 4 B | Checksum; a torn or corrupted record is detected on read |
| padding | rest | Room for new fields without changing the record size |
Who may write the journal: fencing with an epoch. If the engine instance is slow rather than dead, and we start a replacement, two engines could append to the journal at once. Each engine incarnation has an epoch number. A new engine first asks the journal nodes to move to epoch e + 1; a node that has moved rejects appends from epoch e. The new engine needs two of the three nodes to agree, and the old engine needs two of three to accept its appends. Any two pairs out of three nodes share at least one node, so once the new engine holds two, the old one can never collect two acceptances again, even if it's still running and hasn't heard the news. Then the new engine reads the journal from the nodes it holds, takes the longest copy, and re-replicates any tail that's on only one node. This is the same majority logic consensus protocols use (Distributed Consensus: Raft and Paxos).
Replay to a point in time. A regulator asks: "What did XYZ's book look like at 11:02:17?" We don't replay the whole day. We load the snapshot taken before that moment (at most 10 minutes earlier), replay the journal up to the last input stamped before 11:02:17, and stop. With snapshots every 10 minutes, no question needs more than 10 minutes of inputs replayed. That's the general cure for event-sourced systems with long histories: snapshots bound replay, and the event log keeps every earlier state reachable.
Primitives: Write-Ahead Log and LSM Trees (append, flush, then apply) · Event Sourcing and CQRS (state as a fold over events, snapshots)
Step 1.5: Participants Need to See the Book
The problem: members want the top of book and the full depth for every instrument, updated as it changes. Two hundred member applications and data subscribers want it at once. What would you do?
Synthesizing vector architecture diagram...
The engine writes each message once. Fan-out, gap-fill and snapshots all happen on the market-data servers, so a slow or recovering subscriber costs the engine nothing.
Step 1.6: An Order for More Money Than the Member Has
The problem: a member's algorithm has a bug and sends buy orders for 2 million shares, worth about $200 million, while its agreed credit limit with the exchange's clearing arrangement is $5 million. Nothing stops it. Another member fat-fingers a quantity of 1,000,000 instead of 1,000. What would you do?
Round 1 Step Summary
| Step | Problem | Component | What it costs us |
|---|---|---|---|
| 1.0 | (baseline) | Orders in a SQL table, matched by queries | Slow; concurrent matches race |
| 1.1 | Matching by SQL is slow and racy | In-memory book: sorted levels, FIFO queues, order-ID index | State lives in memory |
| 1.2 | Two threads filled one order | One writer thread per book, 4 instrument groups | One instrument is capped by one core |
| 1.3 | Which order came first? | Sequencer: one gap-free sequence; stamped time | On the critical path |
| 1.4 | The engine crashed | Journal of sequenced inputs, 2-of-3 quorum across AZs, snapshots, replay, epoch fencing | Journal latency; replay time |
| 1.5 | Participants need the book | Sequenced incremental market data; gap-fill and snapshots on separate servers | A separate distribution path |
| 1.6 | Orders nobody can pay for | Pre-trade risk in the member's home gateway | Risk state must be fast, consistent, rebuildable |
R1.6 Architecture v1
Now the concepts get AWS names.
Synthesizing vector architecture diagram...
Follow an order: the NLB passes the TLS session to the member's home gateway, which authenticates and runs risk checks, then sends it to the engine. The sequencer numbers it, the journal nodes store it (two of three must flush), a matching thread matches it, and only then do the execution report and market data go out. The spare engine in another AZ does nothing until it's needed.
The pieces:
- Gateways: 3 EC2 instances (
c7i.large), one per AZ, behind a Network Load Balancer (a layer-4 load balancer; it terminates TLS here). A member's session is routed to its home gateway; we give each gateway its own listener port so the mapping is explicit. - Engine: one
c7i.2xlarge(8 vCPUs) in AZ a. One sequencer thread, four matching threads, one output thread that talks to the gateways and market-data servers, and one thread writing to the journal nodes. - Journal nodes: 3
c7i.large, one per AZ, each with an EBS io2 volume (block storage that AWS replicates inside its AZ, with sub-millisecond latency). Each appends, flushes, and acknowledges. They also seal and upload finished journal segments to S3 every few minutes. - Market-data servers: 2
c7i.largein two AZs, behind their own NLB. - Spare engine: one
c7i.2xlargein AZ b, running but not processing. On failover it fences the journal (epoch e + 1), loads the latest snapshots from S3, replays, and takes over. - Post-trade: a small writer that copies trades from the output stream into Aurora PostgreSQL for the end-of-day clearing file. It is off the critical path: if it falls behind, trading doesn't notice.
The trades table, keyed so a replay can't insert a trade twice:
sqlCREATE TABLE trades ( trade_id TEXT PRIMARY KEY, -- 'T-5001-1': input sequence + fill counter, deterministic trade_date DATE NOT NULL, sequence BIGINT NOT NULL, instrument TEXT NOT NULL, price_ticks BIGINT NOT NULL CHECK (price_ticks > 0), quantity BIGINT NOT NULL CHECK (quantity > 0), buy_member TEXT NOT NULL, sell_member TEXT NOT NULL, buy_order_id TEXT NOT NULL, sell_order_id TEXT NOT NULL, aggressor_side CHAR(1) NOT NULL CHECK (aggressor_side IN ('B','S')) ); CREATE INDEX ON trades (trade_date, instrument);
The writer inserts with ON CONFLICT (trade_id) DO NOTHING, so replaying the output after a crash is harmless. Nothing references this table by foreign key, so the retention job (delete rows older than the retention period, by trade_date, in batches) is never blocked.
Trace 1: a limit order rests, then a marketable order matches it.
Synthesizing vector architecture diagram...
The engine may compute the match while the journal flush is in flight, but it releases nothing about input 5001 until two journal nodes have it. The member hears "filled" only after the input that caused the fill is safe.
Trace 2: a crash and replay. At 11:14:03 the engine instance stops responding. The spare's health check misses heartbeats for 10 seconds and starts a takeover:
- The spare asks the journal nodes to move to epoch 8. Nodes b and c agree; node a (same AZ as the dead engine) doesn't answer. Two of three is enough, and the old engine can no longer get two acknowledgments.
- It reads the journal from b and c. The longest copy ends at sequence 612,877; node b's copy is one record shorter, so it copies record 612,877 to b.
- It loads the four snapshots from 11:10 (taken at about sequence 592,000) from S3 and replays about 21,000 inputs, well under a second of work.
- It regenerates the outputs for the replayed inputs and resends the tail to the gateways and market-data servers, which drop sequence numbers they've already seen.
- Gateways reconnect to the new engine (they learn its address from the journal nodes, which record the current epoch's owner) and resend any input they hadn't had acknowledged. The sequencer recognizes resends by
(gateway_id, gateway_seq).
Members saw about 15 to 20 seconds of 503s and no lost acknowledgment.
R1.7 Numbers
Traffic.
The open and close are the busy minutes. We assume the peak second is 10 times the average and plan for a round number:
We assume the busiest instrument carries 10% of the flow: about 100 inputs a second at peak on one book. We assume 10% of inputs produce a trade: about 200,000 trades a day.
Journal.
Peak write rate: to each of three journal nodes. Trivial for any disk; what matters is the latency of each flush.
Book memory per instrument. We size each instrument's pool for 5,000 resting orders (a choice with headroom). Each order node is 64 bytes, the index is an open-addressing hash table kept at most half full, and price levels are small:
| Part | Math | Size |
|---|---|---|
| Order nodes | 5,000 × 64 B | 320 KB |
| Order-ID index | 16,384 slots (the power of two above 10,000) × 16 B | 256 KB |
| Price levels | ~1,000 levels × 64 B | 64 KB |
| Per instrument | ~640 KB |
For 100 instruments: about 64 MB, sized for the worst case; the live books are far smaller. It fits easily in an 8 vCPU instance's 16 GB of memory.
Replay time. A snapshot every 10 minutes means a replay covers at most 10 minutes of inputs. At the peak rate that's inputs. We estimate a replay thread applies at least 500,000 inputs a second (it reads a sequential file and does in-memory work, with no network waits; an estimate to benchmark), so at most about 1.2 seconds. A whole day's 2 million inputs would take about 4 seconds, which is why snapshots matter less at this scale than in Round 2.
Snapshot pause. Each matching thread serializes its 25 books between two inputs. At about 16 MB per group at worst and several GB/s of memory bandwidth, that's a pause of a few milliseconds every 10 minutes per group (an estimate). At millisecond latency targets, that's acceptable; Round 2 removes it.
Latency (P99 estimate, dependent steps add, parallel steps take the max).
| Step | P99 (estimate) | Notes |
|---|---|---|
| Gateway: TLS already open, parse JSON, risk | 0.3 ms | |
| Gateway to engine, same Region | 0.5 ms | Cross-AZ for two of three gateways |
| Sequence ‖ match | 0.1 ms | Match runs while the journal flushes |
| Journal: 2 of 3 flushed | 1.5 ms | Node a in the engine's AZ plus the faster of b and c; io2 flush plus a cross-AZ round trip |
| Release: engine to gateway to member | 0.6 ms | |
| Total inside our front door | ~3 ms | The step that matters is the cross-AZ quorum |
The match and the journal flush run in parallel, so they count as max(0.1, 1.5) = 1.5 ms, not their sum.
Availability. Trading hours are hours, or 8,190 minutes, a month. At a 99.95% target that's minutes of downtime a month. One engine failover (about 20 seconds) uses about 8% of it.
Monthly cost (us-east-1 on-demand prices, 730 hours a month):
| Item | Math | Monthly |
|---|---|---|
Engine + spare, 2 × c7i.2xlarge | 2 × $0.357/h × 730 h | ≈ $521 |
Gateways, 3 × c7i.large | 3 × $0.08925/h × 730 h | ≈ $195 |
Journal nodes, 3 × c7i.large | 3 × $0.08925/h × 730 h | ≈ $195 |
Market-data servers, 2 × c7i.large | 2 × $0.08925/h × 730 h | ≈ $130 |
| EBS io2, 3 volumes of 100 GB with 3,000 IOPS | 3 × (100 × $0.125 + 3,000 × $0.065) | ≈ $623 |
| 2 Network Load Balancers | 2 × $0.0225/h × 730 h + a few load-balancer capacity units | ≈ $60 |
Aurora PostgreSQL, 2 × db.r6g.large | 2 × $0.26/h × 730 h | ≈ $380 |
| S3 (snapshots, journal archive) | ~65 GB a year, a few dollars | ≈ $5 |
| Data transfer out (market data) | ~2 TB/month (below) × $0.09/GB | ≈ $180 |
| Cross-AZ traffic | Journal copies and gateway traffic, a few GB a month × $0.02/GB | < $5 |
| CloudWatch, logs, alarms | an estimate | ≈ $100 |
| Total | ≈ $2,400/month |
The data-transfer line: we assume 200 subscribers each receive about 1.2 market-data messages per input at about 200 bytes of JSON each, during trading hours only: KB/s per subscriber, times 200 subscribers is 4.1 MB/s, times 23,400 s times 21 days ≈ 2.0 TB a month.
The biggest line is the io2 IOPS we provision on the three journal volumes. It buys what we actually need: flushes that complete in well under a millisecond.
R1.8 Trade-Offs
| Choice | We chose | What we give up |
|---|---|---|
| Single writer vs locking | One thread per book | One instrument can't use more than one core. We gain determinism for free and lose nothing at 100 inputs a second per instrument. |
| Event sourcing vs state in a database | Journal of inputs, books rebuilt by replay, snapshots every 10 minutes | Recovery is a replay (seconds), and every engine change must stay deterministic. We gain a complete, ordered history of everything that ever happened, which is also exactly what an auditor wants. A database of current state would answer "what is the book now?" but not "why", and it would put a database write on every order. |
| Journal quorum across AZs vs one local disk | 2 of 3 journal nodes, one per AZ | About 1.5 ms on every order. In exchange, losing any one disk, instance or AZ loses nothing that was acknowledged. |
| Idle spare vs a standby that follows along | Idle spare that replays on takeover | About 20 seconds of downtime per failover. A standby applying every input in lockstep would take over in milliseconds, but it needs strict determinism rules and careful fencing. That's Round 2. |
| Order types | Limit, market, cancel; DAY and IOC | Members want more (modify in place, stops, icebergs). Each is another path to test for determinism, so we add them later, one at a time. |
Drill: The Banking Ledger That Took 3 Days to Replay (snapshots bound replay, as in step 1.4; why a replayable history beats state-in-place for audit, as in this table)
R1.9 Failure Modes
| Failure | What you'd see | How the design responds |
|---|---|---|
| Engine instance crashes | Heartbeats stop; gateways get connection errors | The spare fences the journal with a new epoch, loads snapshots, replays, and resends the output tail; members see 503 for about 20 seconds and resend with the same client order IDs (Trace 2). |
| A journal disk or node fails | One journal node stops acknowledging | The other two still form a quorum; latency may rise slightly (now it waits for the slower of the two). We replace the node, which copies the day's journal from a peer before rejoining. If two of three fail, the engine can't make inputs durable and stops accepting orders: we fail closed rather than acknowledge what we could lose. |
| An AZ fails | The engine's AZ goes dark | Same as an engine crash: the journal quorum survives in the other two AZs and the spare in AZ b takes over. Two gateways remain; the members homed on the lost gateway are moved by an operator. |
| A slow market-data consumer | One subscriber's send buffer fills | The market-data server disconnects it; it reloads from a snapshot. The engine never sees it. |
| A gateway resends an input the engine already sequenced | Duplicate (gateway_id, gateway_seq) | The sequencer drops it and the engine resends the stored outputs for that input. |
| Poison input (a message that crashes the matching code) | The engine crashes, the spare replays, and crashes on the same input | Determinism cuts both ways: a replay hits the same bug. The runbook halts the instrument, and the input is cancelled by an operator CONTROL input with its own sequence number, so the fix is itself in the journal. |
R1.10 Pillar Check
| Pillar | What Round 1 covers |
|---|---|
| Reliability | Journal quorum across three AZs; replay from snapshots; epoch fencing so two engines can't both write; idempotent resends by client order ID and gateway sequence; fail closed when durability is impossible REL 9 · REL 10 · REL 11 |
| Performance Efficiency | In-memory books with O(1) cancel; one writer per book, no locks; group commit; ~3 ms P99 estimated, dominated by the cross-AZ quorum PERF 1 · PERF 3 |
| Security | TLS sessions; authenticated members with per-session tokens; pre-trade credit and fat-finger checks before an order can touch a book SEC 2 · SEC 9 |
| Cost Optimization | About $2,400 a month, derived; the largest line (io2 IOPS) buys the flush latency we need COST 5 · COST 6 |
| Operational Excellence | Light this round: alarms on journal quorum health, engine heartbeat, replay time, and market-data disconnects OPS 8 |
| Sustainability | Light this round: ten small instances; the spare idles at low power SUS 2 |
R1.11 Round 1 Rubric and Follow-Ups
What a strong mid-level (L5) answer shows
- Draws the order book correctly: sorted levels, FIFO queues, an index for O(1) cancel, and matching at the resting order's price.
- Explains why locks break fairness, and chooses one writer per book.
- Defines "first" with a sequencer, not with timestamps from many machines.
- Journals inputs before releasing outputs, and recovers by replaying from a snapshot.
- Publishes market data as a sequenced stream with gap recovery, and never lets a consumer slow the engine.
- Puts risk checks before the sequencer.
Follow-up questions
-
"Why journal the inputs and not the trades?" Answer: the inputs determine everything; the trades are a consequence. From inputs we can rebuild the book, the trades and every market-data message. From trades alone we couldn't rebuild resting orders or cancels. And inputs are the smaller, simpler record: one per order, whatever it caused.
-
"A member sends a cancel and, a millisecond later, its order is filled. The member says the cancel was first." Answer: both are inputs with sequence numbers, and the journal shows which was sequenced first. If the fill's input (the other member's order) has the lower number, the cancel was too late, and we reply
TOO_LATE_TO_CANCEL. The sequence number is the whole argument. -
"Why not use Kafka as the journal?" Answer: we could: a topic with one partition, replication factor 3 and
acks=allgives an ordered, replicated log, and Round 3 uses a Kafka stream for post-trade. For the journal itself we want control over two things: the flush policy (Kafka relies on replication and doesn't flush each write to disk by default) and the epoch fence tied to our engine's takeover. A small purpose-built journal is simple enough to own.
Interview gotchas from this round's wrong answers
| Gotcha | Why it's wrong |
|---|---|
| "Row locks on the best order" | The top of the book is a hot spot by design; lock order becomes priority order. |
| "A lock around the book" | The scheduler decides who's first; replays differ. |
| "Sort by gateway timestamps" | Clocks disagree, and you can't know when every earlier order has arrived. |
| "Save the book to a database on every trade" | Database round trips on every order, and two copies that can disagree. |
| "Let clients query the engine" | Reads compete with the one component that can't scale out. |
| "Check credit in the matching thread" | Credit is per member across instruments, and every check delays everyone's orders. |
| "Store prices as floats" | Two equal-looking prices may not compare equal, and priority depends on equality. |
Round 2 · Senior · "10,000 Symbols, Microseconds, and No Data Loss"
~40 min · Senior SDE (L6) · 1 colocation site, hot standby per engine · 500M orders/day, ~214K/s peak second · matching P99 < 50 µs (target) · RPO 0
R2.0 Where We Left Off
This is what the candidate says aloud in the first 60 seconds of Round 2. If you're starting here, it's everything you need from Round 1.
Round 1 in 60 seconds. "We built a small exchange for 100 instruments: 2 million inputs a day, about 1,000 a second at peak, millisecond latency, in one AWS Region across three AZs. Each instrument has an in-memory order book: sorted price levels, a FIFO queue at each level, and an order-ID index for O(1) cancel; trades happen at the resting order's price. One thread owns each book, so there are no locks and the result doesn't depend on the scheduler. A sequencer gives every input one gap-free number and a timestamp; 'first' means 'lower sequence number'. Every sequenced input is journaled to two of three journal nodes, one per AZ, before any output about it is released, and a new engine fences the old one with an epoch and rebuilds by loading snapshots and replaying. Market data is a sequenced incremental stream fanned out by separate servers with gap-fill and snapshots. Pre-trade risk runs in each member's home gateway, before the sequencer. About $2,400 a month. Open costs: 3 ms is far too slow for the next customers, a failover takes about 20 seconds, and one instance holds every book."
Architecture v1, compact
Synthesizing vector architecture diagram...
Round 1 in one picture: one sequence, one writer per book, and a journal that makes the books rebuildable.
Round 1 step summary
| Step | Problem | Component | What it costs us |
|---|---|---|---|
| 1.1 | SQL matching is slow and racy | In-memory book, FIFO levels, order-ID index | State in memory |
| 1.2 | Two threads filled one order | One writer per book | One core per instrument |
| 1.3 | Which came first? | Sequencer | On the critical path |
| 1.4 | Crash loses the book | Journal quorum, snapshots, replay, epochs | Journal latency |
| 1.5 | Everyone wants the book | Sequenced market data with recovery | A distribution path |
| 1.6 | Orders nobody can pay for | Risk in the home gateway | Consistent risk state |
Open costs: milliseconds of latency, mostly the cross-AZ journal; a 20-second failover; one engine instance for everything.
R2.1 The Scope Raise
Interviewer: "The regional exchange is becoming a national equities exchange. Ten thousand symbols, 500 million orders and cancels a day, and the first and last minutes of the day are brutal. Our members are trading firms that colocate their servers in our data center, and they measure us in microseconds. We can't lose an order, and a failover must look like a hiccup, not an outage. Market data goes to about a thousand direct subscribers, who must be able to recover anything they miss. And after a bad afternoon at another venue, we need protection against runaway prices."
| We ask | Interviewer answers | What it changes in the design |
|---|---|---|
| Where do members' servers run? | In our data center, a few racks from our engines. | We move the core out of the AWS Region to a colocation site: microseconds are about metres of cable, and the Region is hundreds of kilometres from these members. Round 3 revisits where each piece should live. |
| How many trades? | About 50 million a day, one for every 10 orders. | Most inputs are adds and cancels that never trade. Cancels must be as cheap as possible. |
| How bursty is the open? | The busiest second is about ten times the day's average, and inside that second some symbols see hundreds of orders in a millisecond. | We size per-partition capacity for millisecond bursts, not for averages (R2.6). |
| What latency do members expect? | Wire to wire, under 50 microseconds at P99, as a target we publish. | Every layer we pass through must cost single-digit microseconds: no garbage collection, no locks, no kernel network stack (steps 2.1 and 2.2). |
| Can we lose an acknowledged order if a server dies? | No. | Every input must be on two machines before we release anything about it (step 2.3). |
| How long can a failover take? | Members must barely notice: milliseconds. | A standby that is already up to date, not one that replays (step 2.4). |
| Do we keep all order types from Round 1? | Yes. Still DAY and IOC only, so books are empty at night. | We can move symbols between engines between trading days without moving live orders (step 2.7). |
Scope change
| Round 1 | Round 2 | |
|---|---|---|
| Symbols | 100 | 10,000 |
| Inputs | 2M/day, ~1,000/s peak | 500M/day; 21,367/s average; ~214K/s peak second; 200 inputs in 1 ms on the busiest partition |
| Latency | ~3 ms | Matching P99 < 50 µs wire to wire (a published target) |
| Failover | ~20 s (replay) | ~5 ms (a target), no lost acknowledgments |
| Market data | ~200 WebSocket subscribers | ~1,000 direct subscribers, redundant feeds, gap-fill and snapshots |
| Protection | Credit and fat-finger checks | + price bands and trading pauses |
| Where | 1 AWS Region, 3 AZs | 1 colocation site, members colocated |
| Availability | 99.95% of trading hours | 99.999% of trading hours (a target): about 59 s a year |
R2.2 What Breaks in the Round 1 Design
| Round 1 choice | What breaks at the new scope |
|---|---|
| A Region hundreds of kilometres from members | Light in fibre covers about 200 km per millisecond. The round trip alone is milliseconds, before we do anything. |
| Objects allocated as orders arrive | A memory allocator or garbage collector can pause a thread for milliseconds. One pause at the open delays thousands of orders. |
| Threads handing work over through queues with locks | Each handoff can put a thread to sleep and wake it later: microseconds each, sometimes far more. |
| The kernel's network stack | System calls, interrupts and copies cost several microseconds per packet and add jitter. |
| A flush to three AZs before every release | 1.5 ms per order: 30 times our whole budget. |
| An idle spare that replays | About 20 seconds of downtime. The new availability target allows about 59 seconds a year. |
| Market data over TCP WebSockets from two servers | A thousand subscribers each getting their own copy of the full feed; one slow reader holds up a TCP stream; no redundancy. |
| One engine for everything | A burst in one symbol delays every other symbol behind it. |
| No price protection | A large market order in a thin book can trade at absurd prices in microseconds. |
R2.3 New Requirements and API Additions
Binary order entry. Members now send fixed-size binary messages over a persistent TCP session instead of JSON over HTTPS. Parsing is a few loads from known offsets instead of scanning text, and the message is about a fifth of the size. The field layout below is ours; it follows the style of real exchange protocols such as Nasdaq's OUCH (order entry) and ITCH (market data), and of Simple Binary Encoding (SBE), a binary message standard from the FIX Trading Community.
Enter Order, 36 bytes, little-endian:
| Field | Type | Size | Notes |
|---|---|---|---|
msg_type | char | 1 B | O = enter order |
session_seq | uint64 | 8 B | The member's own message counter on this session; the gateway rejects a gap and asks for a resend |
client_order_id | uint64 | 8 B | Unique per member per day; resends are recognized by it |
symbol_id | uint32 | 4 B | From the daily reference file; faster to route than a ticker string |
side | char | 1 B | B or S |
order_type | char | 1 B | L limit, M market |
time_in_force | char | 1 B | D day, I immediate-or-cancel |
quantity | uint32 | 4 B | Shares |
price | int64 | 8 B | Fixed point, 4 decimal places: 185.50 is 1,855,000 |
Cancel Order is 21 bytes: msg_type X, session_seq, client_order_id, and an optional quantity to reduce by (0 = cancel all).
The sequenced input record (the journal record, and what the standby receives), exactly 64 bytes, one cache line:
| Field | Type | Size |
|---|---|---|
partition_seq | uint64 | 8 B |
epoch | uint32 | 4 B |
session_id | uint32 | 4 B |
session_seq | uint64 | 8 B |
timestamp_ns | uint64 | 8 B |
kind, side, type_tif | 3 × uint8 | 3 B |
symbol_id | uint32 | 4 B |
quantity | uint32 | 4 B |
price | int64 | 8 B |
client_order_id | uint64 | 8 B |
crc32c | uint32 | 4 B |
| padding | 1 B | |
| Total | 64 B |
Round 1's 128-byte record carried a gateway ID and sequence; here session_id plus session_seq identifies every input uniquely, so a resend after a failover is recognized. kind also covers the non-order inputs the sequencer injects: TIMER (step 2.6), BAND_UPDATE (step 2.6), CHECKPOINT (step 2.4) and operator CONTROL messages.
Market data. UDP packets, each with a 20-byte header (channel_session 10 B, first_seq uint64 8 B, msg_count uint16 2 B, in the style of Nasdaq's MoldUDP64) followed by one or more messages. Every message has a sequence number: the header's first_seq plus its position in the packet.
| Message | Fields | Size |
|---|---|---|
| Add Order | type, timestamp_ns, order_id, side, quantity, symbol_id, price | 34 B |
| Order Executed | type, timestamp_ns, order_id (resting), executed_qty, trade_id, price | 37 B |
| Order Reduced | type, timestamp_ns, order_id, reduced_qty | 21 B |
| Order Deleted | type, timestamp_ns, order_id | 17 B |
| Trading Status | type, timestamp_ns, symbol_id, status (trading, limit state, paused, halted), reason | 15 B |
We publish one channel per engine partition (16 channels, step 2.7), each on two independent feeds, A and B.
Recovery requests. Over TCP to a recovery server:
json{ "type": "RETRANSMIT", "channel": 7, "from_seq": 48211903, "count": 212 }
It returns the messages as originally sent, byte for byte. At most 10,000 messages per request and a per-member request rate (our choice), so recovery traffic can't become its own storm.
json{ "type": "SNAPSHOT", "channel": 7 }
It returns every resting order on the channel, stamped with as_of_seq: the last channel sequence number the snapshot includes.
Collar parameters per symbol, in the daily reference file (YAML):
yamlsymbol_id: 4417 ticker: XYZ tick: 100 # price units: 0.01 in 4-decimal fixed point luld_tier: 1 max_order_qty: 250000 max_order_notional_usd: 50000000 entry_collar_pct: 10 # reject limit orders priced more than 10% through the band's reference price
R2.4 Design Evolution: Microseconds and No Loss
Step 2.1: Millisecond Pauses From Memory Allocation
The problem: in load tests, the median match is 2 µs, but once every few seconds a match takes 3 to 8 ms. The profile shows the time in the memory allocator and, in a managed-runtime build, in garbage collection. At the open, one such pause queues thousands of orders behind it. What would you do?
An order node, laid out to fit one cache line:
| Field | Size | Field | Size |
|---|---|---|---|
order_id | 8 B | prev, next (pool indexes) | 8 B |
client_order_id | 8 B | level (index) | 4 B |
session_id | 4 B | timestamp_ns | 8 B |
price | 8 B | symbol_id | 4 B |
quantity, original_qty | 8 B | side, flags, padding | 4 B |
| Total | 64 B |
Step 2.2: Threads Hand Work Over Slowly
The problem: a packet arrives at the network card, goes through the kernel, into the sequencer thread, through a locked queue to the matching thread, and through another locked queue to the thread that sends the results. Each handoff costs a few microseconds on a good day, and occasionally a thread is descheduled and a handoff takes hundreds of microseconds. What would you do?
Synthesizing vector architecture diagram...
One partition's pipeline on one engine host. The matcher and the journal read the same input ring at the same time; the publisher waits for both before releasing anything.
Step 2.3: Durability Costs a Flush per Order
The problem: Round 1 flushed every input to two of three journal nodes before releasing output. Across AZs that was 1.5 ms. Even on a local NVMe drive, a flush per input would cap us and add tens of microseconds each. What would you do?
Primitive: Write-Ahead Log and LSM Trees (group commit: many appends, one flush)
Step 2.4: The Primary Died Mid-Burst
The problem: at 09:30:02 an engine host's power supply fails. Its four partitions (a quarter of all symbols) stop. Round 1's recovery (fence, load snapshot, replay) would take seconds even with everything local, and members' algorithms are firing orders every microsecond. What would you do?
Why three arbiter nodes, and what a partition does to them. If one arbiter node is cut off from the other two, it's a minority: it can't commit anything, so it can't promote anybody. The two connected nodes still form a majority and keep working. Three nodes survive one failure; five would survive two, at the cost of two more servers and a slightly slower commit. The arbiter isn't on the order path and is consulted only at failover, so we choose three.
Synthesizing vector architecture diagram...
An engine's roles. Every arrow into Primary or Solo goes through the arbiter, so at most one engine per partition can release output in any epoch.
Primitive: Distributed Consensus: Raft and Paxos (the arbiter's majority and terms)
Drill: The Network Partition That Elected Two Leaders (why a minority can't promote anyone; three nodes versus five)
Step 2.5: Participants Lost Market-Data Packets
The problem: a thousand subscribers want every market-data message within microseconds. During a burst, a member's switch drops 20 packets. Their copy of the book is now wrong, and their algorithm trades on it. What would you do?
Synthesizing vector architecture diagram...
Arbitration hides most losses for free. Only a loss on both feeds costs a round trip to the retransmission server, and the subscriber keeps buffering the live feed while it waits.
Step 2.6: A Huge Market Order Swept the Book Down to 0.01
The problem: a member's algorithm sends a market sell order for 500,000 shares of a thinly traded stock. The bids run out after a few levels, and the order keeps selling into whatever bids remain, down to a stray bid at 0.01. The stock "crashes" 99% in 40 microseconds. What would you do?
The swept book, with and without bands. Bids for a thin stock, reference price 20.00, Tier 2, so a 10% band: 18.00 to 22.00.
| Bid level | Quantity | Without bands | With a 10% band |
|---|---|---|---|
| 19.90 | 3,000 | fills | fills |
| 19.50 | 5,000 | fills | fills |
| 18.10 | 2,000 | fills | fills |
| 17.00 | 4,000 | fills | not reached: 17.00 is below 18.00 |
| 0.01 | 100 | fills | not reached |
| Sold | 14,100 of 500,000, last trade at 0.01 | 10,000 of 500,000, lowest trade 18.10; 490,000 cancelled back |
Step 2.7: One Symbol's Burst Slows the Others
The problem: a news story breaks on one company, and its symbol receives 150,000 orders in the next second. Every other symbol that shares its engine thread waits behind them, and members in unrelated stocks see latency jump from 20 µs to 200 ms. What would you do?
Primitive: Database Sharding and Partition Keys (partitioning by load, isolating hot keys)
Drill: One Customer, One Shard, One Outage (a hot symbol gets its own partition; cross-partition questions go to the post-trade store)
Round 2 Step Summary
| Step | Problem | Component | What it costs us |
|---|---|---|---|
| 2.1 | Allocation pauses | Preallocated 64-byte order pools, price-indexed level arrays, locked memory | Fixed capacity, harder code |
| 2.2 | Slow handoffs | Lock-free rings (Disruptor), pinned isolated cores, kernel bypass, no false sharing | Busy-polling cores |
| 2.3 | A flush per order | Batched local journal + replication to a standby; release when both have it | Latency of the slower of the two |
| 2.4 | Primary died | Lockstep hot standby, state digests, arbiter with epochs, fence by withheld acknowledgments | Double hardware; strict determinism |
| 2.5 | Lost market-data packets | Multicast feeds A and B, sequence numbers, retransmission and snapshot servers | More services; client recovery logic |
| 2.6 | A runaway market order | Entry collars, LULD bands, limit states and pauses driven by TIMER inputs | Some orders rejected or cut short |
| 2.7 | One symbol slows others | 16 partitions by load, hot symbols isolated, ring-based backpressure | Nightly rebalancing; rejections in extreme bursts |
R2.5 Architecture v2
Synthesizing vector architecture diagram...
Follow an order: a gateway checks it and sends it to its symbol's partition on a primary host. The sequencer numbers it; the matcher, the local journal and the standby all get it at once; results are released to the gateway and both feeds only when the journal and the standby have it. The arbiter is consulted only when something fails.
Component counts: 10 gateways (8 needed for about 4,000 sessions at 500 each, plus 2 spare), 8 engine hosts (4 primary, 4 standby; 16 partitions), 4 recovery servers (2 on each feed network), 3 arbiter nodes, 2 post-trade servers: 27 servers.
Latency budget, wire to wire (a target to measure, not a promise any platform makes by itself; dependent stages add, parallel stages take the max):
| Stage | P99 target | What it covers |
|---|---|---|
| 1. Gateway | 10 µs | Kernel-bypass receive, parse, session sequence check, risk checks |
| 2. Sequencer | 5 µs | Hop to the engine host; stamp sequence number and time; write the input ring |
| 3. Ring handoff | 2 µs | The matcher sees the new slot |
| 4. Match ‖ journal ‖ standby | max(8, 15, 15) = 15 µs | Matching 8 µs, local journal write 15 µs and standby round trip 15 µs, all in parallel |
| 5. Publish | 10 µs | Build the execution report and market-data packets; send to the gateway, out to the member, and onto feeds A and B |
| Total | 42 µs | Under the 50 µs P99 target, with 8 µs of margin |
The journal write and the standby round trip are the two stages to watch: they set the release time. If they ever sat in series instead of in parallel, the budget would be 57 µs and blown.
Trace 1: an order at the open.
Synthesizing vector architecture diagram...
The publisher is the gate: it holds the matcher's outputs until the journal core says the input is durable locally and on the standby. At 09:30:00 the journal batch may carry dozens of inputs, but each is released as soon as its own batch completes.
Trace 2: a failover. The primary host for partitions 5 to 8 loses power at 09:30:02.000000.
| Time after failure | Event |
|---|---|
| 0 | The primary stops. Its last released input on partition 7 is 48,290,114; the standby has acknowledged through 48,290,117 (three inputs were in flight to the publisher). |
| 2.0 ms | The standby has heard no heartbeat for 2 ms, stops acknowledging epoch 41, and asks the arbiter for epoch 42. |
| ≤ 3.0 ms | The arbiter commits epoch 42 for partitions 5 to 8 (a majority write; ≤ 1 ms is our estimate on a local network). |
| ≤ 3.1 ms | The new primary finishes the inputs left in its ring, turns publishing on, and re-sends its last second of output. Consumers drop sequence numbers they already have; inputs 48,290,115 to 48,290,117 are released for the first time. |
| ≤ 5.0 ms | The gateways, told of epoch 42, resend every input that wasn't acknowledged. Resends already sequenced are recognized by (session_id, session_seq) and answered from the stored outputs; the rest get new sequence numbers. Trading resumes. |
About 5 ms end to end (a target, proven in failover drills, never assumed). No released message is lost and none is released twice with different content, because the new primary computed the same outputs for the same inputs.
Trace 3: a packet gap recovered. This is the sequence diagram in step 2.5: a burst drops sequence 100 on both feeds; the subscriber buffers 101, asks a retransmission server for 100, applies it, then applies 101. With a retransmission server in the same site, recovery takes well under a millisecond (an estimate).
R2.6 Numbers and Cost
Traffic.
Reconciling "200,000 a second per partition" with that: the 200K figure is not the exchange's average or even its busy-second rate spread over partitions. It's the burst rate one partition must absorb for a few milliseconds at the open (we assume the busiest partition can see 200 inputs within one millisecond), and so it's our capacity target per partition core: 200K inputs/s sustained, or 5 µs of work per input on average.
| Scope | Rate | Where it comes from |
|---|---|---|
| Exchange-wide, daily average | 21,367/s | 500M ÷ 23,400 s |
| Exchange-wide, busiest second | ~214K/s | 10 × average (assumption) |
| Per partition, busiest second | ~13.4K/s on average across 16; the hottest ~40K/s | 214K ÷ 16; hot partitions assumed about 3 × the average |
| Per partition, 1 ms microburst | up to 200K/s (200 inputs in 1 ms) | assumption; sets the core's capacity target |
| One symbol | at most one core, whatever happens | one writer per book |
Can one core do 200K/s? A match that trades is budgeted at 8 µs P99; most inputs (90%) are adds or cancels that don't trade. If a trading input averages 3 µs and a non-trading one 0.5 µs (estimates to benchmark), the average is µs, about 1.3 million inputs a second, so 200K/s leaves more than 6 times headroom.
Book memory, for 50 million resting orders across the exchange (the average of 5,000 per book × 10,000 books):
| Part | Math | Size |
|---|---|---|
| Live order nodes | 50M × 64 B | 3.2 GB |
| Order-ID hash tables | per partition: sized for a full pool, 2 × 3.125M = 6.25M entries (3.125M is the exchange-wide average per partition; the pool doubles it for hot partitions); at ≤ 50% load that needs 12.5M slots → = 16,777,216 slots × 16 B = 268 MB, so a full pool sits at 37% load; × 16 | 4.3 GB |
| Price-level arrays | 10,000 symbols × 2,048 levels × 16 B, plus a little for the wider windows of high-priced stocks | ~0.33 GB |
| Pool headroom | a second 3.2 GB of free nodes (2× sizing) | 3.2 GB |
| Total | ~11 GB (3.2 + 4.3 + 0.33 + 3.2), ~2.75 GB per engine host (4 of 16 partitions) |
This fits easily in RAM. It does not fit in a CPU's last-level cache (tens to a few hundred MB), and it doesn't need to: the hot part of each book, the few levels near the best prices, stays in cache because it's touched constantly.
Journal.
Peak write rate: MB/s exchange-wide; a 1 ms microburst on one partition is KB in that millisecond. Bandwidth is trivial; what matters is each write's latency. Replication to the standbys is the same 13.7 MB/s, about 110 Mbps.
Ring memory: 65,536 slots × 64 B = 4 MB per input ring; 16 partitions × 2 rings (input and output) × 2 hosts ≈ 256 MB in total.
Replay time. A normal day is M inputs per partition. At an estimated 2M inputs a second per core (no network, sequential reads), a partition replays a whole day in about 16 seconds, and all partitions replay in parallel. The standby also feeds a snapshotter process on its host, which applies the stream to its own copy of the books and writes a snapshot every 10 minutes, so no engine thread ever pauses for a snapshot.
Market-data bandwidth. We assume 1.3 market-data messages per input (an add, delete or execution, plus the extra executions of trading inputs) at 40 bytes on average, and about 10 messages per packet at peak, so the 62 bytes of packet headers (20 B feed header, 42 B Ethernet, IP and UDP) add about 6.2 bytes per message:
Microbursts over a millisecond can run several times higher, so each feed network runs at 10 Gbps. Multicast versus unicast: with multicast we send that 103 Mbps once per feed. As TCP unicast to 1,000 subscribers it would be Gbps per feed, and twice that for A and B. The day's feed is GB of payload, which each retransmission server keeps in memory (so they're 64 GB machines).
Availability. Trading time is s a year. At 99.999%:
A 5 ms failover is 0.005 s. One Round 1-style 20-second recovery would use a third of the year's budget. Five nines is a target; one bad day (a determinism bug, a botched release) can spend it, which is why R2.8 and Round 3's runbook exist.
Hardware cost. Our servers sit in the colocation site, not in an AWS Region (step 2.1's scope answer). As a verified yardstick, here is what the same machines would cost as EC2 bare-metal instances at us-east-1 on-demand list prices; the owned-hardware line below it is a rough estimate.
| Item | Math | Monthly |
|---|---|---|
Engine hosts, 8 × c7i.metal-24xl (96 vCPUs, bare metal) | 8 × $4.284/h × 730 h | ≈ $25,000 |
Gateways, 10 × c7i.metal-24xl | 10 × $4.284/h × 730 h | ≈ $31,300 |
Recovery servers, 4 × r7i.2xlarge (64 GB) | 4 × $0.529/h × 730 h | ≈ $1,550 |
Arbiter nodes, 3 × c7i.large | 3 × $0.08925/h × 730 h | ≈ $195 |
Post-trade servers, 2 × c7i.2xlarge | 2 × $0.357/h × 730 h | ≈ $520 |
| EC2 bare-metal yardstick | ≈ $58,600/month | |
| Owned hardware (rough estimate, unverified) | ≈ $40K/month |
Either way, the platform costs about the same as a few engineers. The expensive resources in this round are engineering discipline (determinism, zero allocation) and the network, not servers.
R2.7 Trade-Offs
Four ways to build a matching engine. Figures are rough orders of magnitude for one symbol's book, to show the shape of the trade-off, not benchmarks.
| Single writer, in memory (chosen) | Multi-threaded book with locks | Distributed actors | Database transactions | |
|---|---|---|---|---|
| Latency per match (rough) | Microseconds | Hundreds of µs to ms under contention | Milliseconds (network hops between actors) | Tens of ms or more (disk and round trips) |
| Throughput per symbol (rough) | Hundreds of thousands/s | Tens of thousands/s | Tens of thousands/s | Low thousands/s |
| Concurrency hazards | None inside a book; care with false sharing | Lock convoys, priority by scheduler luck | Message reordering, split brain between actors | Row-lock waits and deadlocks |
| Determinism and replay | Built in: sequenced inputs, one writer | Lost: thread races decide order | Hard: needs a global order anyway | Order decided by lock acquisition, not recorded as inputs |
| What it needs | Bare metal, pinned cores, discipline | Ordinary servers | A cluster | A big database |
| Choice | We chose | What we give up |
|---|---|---|
| Binary vs text protocols | Fixed-size binary for order entry and market data | Human readability and easy debugging; members need client libraries. We gain parsing in nanoseconds instead of microseconds, and messages about a fifth the size. We keep a JSON gateway for low-volume members (a choice). |
| Lockstep standby vs journal replay | Lockstep standby | Double hardware, and every engine change must obey the determinism rules. Replay alone would take seconds per failover, which the 59-second yearly budget can't afford. We keep replay for audits and for rebuilding a standby. |
| Release after standby + journal vs after journal only | Both | ~15 µs on every release (in parallel with matching). Journal-only would survive a process crash but not the loss of the host. |
| Busy polling vs interrupts | Busy polling on isolated cores | Cores burn power all day even when idle. Interrupt-driven threads sleep and cost microseconds to wake. |
R2.8 Failure Modes
| Failure | What you'd see | How the design responds |
|---|---|---|
| Primary engine host crashes | Heartbeats stop on four partitions | Standby stops acknowledging, arbiter commits a new epoch, new primary publishes and gateways resend (Trace 2); ~5 ms target. The failed host is rebuilt as the new standby from a snapshot plus the journal. |
| Standby host crashes | Acknowledgments stop; releases stall on four partitions | The primary asks the arbiter for a solo epoch and releases on its local journal alone (a stall of about the 2 ms detection time). A spare host loads a snapshot, catches up from the journal, and becomes the standby; until then, a second failure would stop those partitions. |
| Sequence gap on ingress | A member's session_seq jumps from 203 to 205 | The gateway rejects 205 and asks for a resend from 204. It never forwards out of order, so a lost packet can't change priority. |
| Input ring filling | Fill level above 25%, then 50% | Gateways stop reading new orders for that partition, then reject them with BUSY; cancels always pass. Alarm at 25%. |
| Market-data loss | Retransmission requests jump on one channel | Arbitration covers single-feed losses. A spike on both feeds points at our publisher or a shared switch; check drop counters on each feed network. |
| Digest mismatch between primary and standby | The CHECKPOINT digests differ | Someone's state is wrong, and we don't know whose yet. The standby stops acknowledging (so the primary goes solo through the arbiter, a controlled stall) and a separate machine replays the journal to that checkpoint: whichever engine matches the replay is right. If the primary is the wrong one, the operators halt the partition, promote the verified copy, and treat it as a P1 incident (Round 3). |
| Order pool running low | Pool use above 70% on a partition | Alarm; at 100%, new orders on that partition are rejected, cancels still work. Pools are resized at the next start-up. |
R2.9 Production Gotchas
| Gotcha | Why it hurts | What we do |
|---|---|---|
| Allocation on the hot path | Allocator locks and page faults, or garbage collection, pause the matching thread for milliseconds | Preallocate every pool, ring and table; lock and pre-touch memory |
| False sharing | Two cores writing different variables on one 64-byte cache line keep invalidating each other | Put each cursor and hot counter on its own cache line |
| Reading the system clock in the engine | Replays and the standby see different times, so they make different decisions | Time comes only from timestamp_ns in inputs and from TIMER inputs |
| Unbounded buffers | Memory grows until the process dies, and orders wait so long they're stale | Fixed rings; backpressure at 25%, reject new orders at 50% |
| One market-data feed without recovery | A burst drops packets and a subscriber's book is silently wrong | Feeds A and B, per-channel sequence numbers, retransmission and snapshots |
| Iterating a hash table by address | Two hosts iterate in different orders and diverge | Iterate only in a defined order (price, then time), never by memory address |
| A release that changes behaviour mid-day | Primary and standby run different logic; digests diverge | Engine builds change only between sessions, on both hosts together, checked at start-up |
R2.10 Pillar Check
| Pillar | What Round 2 adds |
|---|---|
| Reliability | Lockstep hot standby with ~5 ms failover (target); release only after local journal and standby have each input; an arbiter so only one primary per epoch; digests every second; 16 partitions limit any failure's reach REL 10 · REL 11 · REL 5 |
| Performance Efficiency | 42 µs P99 budget (target) with parallel durability; zero allocation; lock-free rings; kernel bypass; pinned cores; partitions assigned by measured load PERF 2 · PERF 4 · PERF 5 |
| Security | Authenticated binary sessions with sequence checks; pre-trade risk before sequencing; entry collars and LULD bands inside the engine; members reach us only through gateway cross-connects SEC 2 · SEC 5 |
| Cost Optimization | ~$58.6K/month as an EC2 bare-metal yardstick, ~$40K owned (rough); multicast avoids ~100 Gbps per feed of unicast fan-out COST 5 · COST 8 |
| Operational Excellence | Alarms on ring fill, pool use, standby acknowledgments, digest mismatches and retransmission rates; releases only between sessions OPS 6 · OPS 8 |
| Sustainability | Light this round: busy-polling cores draw full power all day; we pin only the cores the pipelines need (four per partition) and leave the rest to power-manage SUS 3 |
R2.11 Round 2 Rubric and Follow-Ups
What a senior (L6) answer adds over L5
- Explains where microseconds go (allocation, handoffs, the kernel, flushes) and removes each one, with a budget that uses max() for parallel stages.
- Uses a lock-free ring with busy-polling pinned consumers, and knows about false sharing.
- States exactly what RPO 0 covers: released means on two hosts.
- Builds a lockstep standby, lists the determinism rules, and verifies them with digests.
- Fences failover so it holds while the news is still spreading, and handles the standby dying too.
- Designs market data for loss: multicast, A/B arbitration, retransmission and snapshots, and knows what AWS does and doesn't offer for multicast.
- Describes LULD-style bands accurately and drives time-based rules from sequenced timer inputs.
- Partitions by load, not by hash, and puts backpressure at the gateways.
Follow-up questions
-
"Why not have the standby vote, so the arbiter isn't needed?" Answer: with only two engines, neither can tell "the other died" from "the link between us died". Each would promote itself. A third party that both must ask (the arbiter's majority) is what breaks the tie. The arbiter isn't on the order path, so it costs nothing until something fails.
-
"A member says its order reached our gateway before a competitor's but was sequenced after it." Answer: that can happen: two gateways process at slightly different speeds, and the sequencer orders by arrival at the sequencer. The promise we make is that the path is the same for everyone (same gateway hardware, same cable lengths, R3.4), not that arrival at different gateways is compared. The journal shows the sequence and the gateway timestamps, so we can show exactly what happened.
-
"Could we make matching itself parallel for one very busy symbol?" Answer: not without giving up price-time priority. Each order's result depends on what the previous order did to the same book. What we can do is take everything else off that core (risk in gateways, journaling and publishing on other cores) and give the symbol a partition of its own.
Interview gotchas from this round's wrong answers
| Gotcha | Why it's wrong |
|---|---|
| "A faster garbage collector" | Shorter pauses are still pauses; at 200K inputs/s they hit thousands of orders. |
| "More threads" | More handoffs; one symbol still has one writer. |
| "Write the journal asynchronously" | A crash loses inputs whose results were already released. |
| "Restart and replay" as the failover plan | Seconds of outage against a budget of about a minute a year. |
| "Detect failure with a timeout and promote" (no fence) | A slow primary and a new primary both release; members see two different histories. |
| "TCP to every subscriber" | A thousand copies per message, unequal delivery times, and slow readers stall streams. |
| "Hash symbols across engines" | Spreads counts, not load; hot symbols collide. |
Round 3 · Architect · "A Regulated Venue: Fairness, DR, and the Cloud"
~45 min · Principal (L7) · primary site + journal bunker + far DR site + an AWS Region · up to 1.5B orders/day, ~641K/s peak second · no released trade lost, even in a site disaster · bit-identical replay for regulators
R3.0 Where We Left Off
This is what the candidate says aloud in the first 60 seconds of Round 3. If you're starting here, it's everything you need from Round 2.
Round 2 in 60 seconds. "We built a national equities exchange in one colocation site: 10,000 symbols, 500 million inputs a day, about 214,000 a second in the busiest second, with members' servers a few racks away. Symbols are split by measured load into 16 partitions; each is a pipeline of four pinned cores connected by lock-free rings: receive and sequence, match, journal and replicate, publish. Nothing allocates during the session. Every input goes to the local NVMe journal in batches and to a hot standby on another host, and its results are released only when both have it, so a released message survives the loss of any one host. The standby runs the same deterministic engine in lockstep with output suppressed; digests every second prove the two agree; a three-node arbiter hands out epochs, and a standby that suspects the primary stops acknowledging first, so the old primary can't release anything while the news spreads. Failover is about 5 ms as a target. Market data is multicast on feeds A and B with retransmission and snapshot servers; LULD bands and pauses run inside the engine on sequenced timer inputs; gateways push back when a partition's ring fills. About 42 µs P99 as a budget. Open costs: one site, audit data that exists but isn't managed as a record, paths that may not be equal, a trade handoff that's only a file, and capacity sized for normal days."
Architecture v2, compact
Synthesizing vector architecture diagram...
Round 2 in one picture: every released message is on two hosts, and one arbiter decides who may publish.
Round 2 step summary
| Step | Problem | Component | What it costs us |
|---|---|---|---|
| 2.1 | Allocation pauses | Preallocated pools and arrays | Fixed capacity |
| 2.2 | Slow handoffs | Disruptor rings, pinned cores, kernel bypass | Busy-polling cores |
| 2.3 | A flush per order | Batched journal + standby; release when both have it | ~15 µs, in parallel |
| 2.4 | Primary died | Lockstep standby, digests, arbiter epochs | Double hardware; determinism rules |
| 2.5 | Lost packets | Multicast A/B, retransmission, snapshots | More services |
| 2.6 | Runaway prices | Collars, LULD bands, timer inputs | Some orders rejected |
| 2.7 | Bursts spill over | 16 partitions by load, backpressure | Nightly rebalancing |
Open costs: everything lives in one building; the journal is on local drives, not in a record store; nothing guarantees equal paths; clearing gets a file; and nothing is sized for an extreme day.
R3.1 The Scope Raise
Interviewer: "We're now a regulated national market, and the rules come with it. Regulators want a complete audit trail and the ability to replay any trading day, years later. Members are complaining about fairness: one firm's rack is closer to our gateways than another's. If our building is lost, a disaster-recovery site must take over, and the board says no trade may be lost. Clearing needs every trade, reliably, for settlement the next business day. The board also asks whether we can move all this to the cloud. And last spring, on a meme-stock day, volume tripled."
| We ask | Interviewer answers | What it changes in the design |
|---|---|---|
| What must the audit trail contain, and for how long? | Every order event, every operator action, and what we published, reproducible exactly. Assume 7 years. In the US, we also report order events to the regulators' Consolidated Audit Trail. | The sequenced journal becomes the system of record, stored immutably with the engine version needed to replay it (step 3.1). |
| What exactly is the fairness complaint? | Cable length inside the building, and firms wanting to know that nobody gets our data first. | Equal paths by construction, and a policy on speed bumps (step 3.2). |
| "No lost trades" even if the whole site is lost? At what latency cost? | Yes: no trade anyone was told about may be lost. Show us the cost and we'll decide. | A second copy in another building before release: the round trip to it goes onto every order (step 3.3). |
| Where can a DR site be, and how fast must we resume? | A site about 500 km away, on a different power grid. Assume our documented target is to resume within 2 hours (we'd confirm it against the rules that apply to us); show us what drills achieve. | Too far for a synchronous copy: we need something in between (step 3.3). |
| When do trades settle? | US equities settle one business day after the trade (T+1). The clearing house needs every trade the same day, exactly once. | A post-trade stream with idempotent consumers, reconciled every night against the journal (step 3.4). |
| Why does the board want the cloud? | Fewer data centers and faster delivery. They also heard another exchange moved markets to AWS. | What can move to a Region and what can't, and what "cloud" means inside a colocation site (step 3.5). |
| How extreme were the meme-stock days? | About 3 times normal volume for the day, and a single stock at 10 to 15% of all messages. | Headroom planning, symbol moves between sessions, and circuit breakers as the last line (step 3.6). |
Scope change
| Round 2 | Round 3 | |
|---|---|---|
| Volume | 500M/day, ~214K/s peak second | Up to 1.5B/day on extreme days, ~641K/s peak second; planned capacity 1.28M/s |
| Partitions | 16 | 32, rebalanced between sessions |
| Sites | One | Primary, a journal bunker ~10 km away, a DR site ~500 km away, and an AWS Region |
| RPO | 0 for any single host | 0 for the loss of the whole primary site; a documented small RPO for a regional disaster |
| RTO | ~5 ms within the site | ~5 ms within the site; ≤ 2 h for a site disaster (documented), ~1 h in drills |
| Audit | Journal on local drives | Immutable journal archive for 7 years; bit-identical replay |
| Clearing | A file | A stream plus a reconciled end-of-day file |
| Latency | 42 µs P99 budget | ~137 µs P99 budget, because of the bunker round trip |
R3.2 What Breaks in the Round 2 Design
| Round 2 choice | What breaks at the new scope |
|---|---|
| One site | A fire, flood or power failure of the building stops the market, and the journal on its drives may be gone. |
| Journal on local NVMe, kept "a while" | Not immutable, not retained for years, and a replay in five years needs the engine code from five years ago. |
| Members' racks wherever there was space | Cable length differences are real microseconds; some firms are systematically first. |
| A clearing file at the end of the day | One late or corrupt file delays settlement for the whole market; nothing proves every trade reached clearing exactly once. |
| 16 partitions sized for normal days | At 3× volume, the hottest partitions run near their core's limit, and a single meme stock can dominate one. |
| Everything on our own hardware | The board's question has no answer yet: what could run in the cloud, and what must stay? |
R3.3 New Requirements and API Additions
Audit records. The sequenced input record (R2.3) is already the main audit record. Three more kinds join the journal and the archive:
| Record | Fields | Why |
|---|---|---|
CHECKPOINT output (every second) | partition, partition_seq, timestamp_ns, book_digest (128-bit), output_chain_hash (256-bit), engine_build (hash of the engine binary) | Lets any replay prove it reproduced the live state at that point |
CONTROL input (operator action) | operator ID, change ticket, action (halt, resume, cancel order, change band), target, reason | Manual actions are inputs too, so a replay repeats them at the same place |
SESSION input | member, session, event (logon, logoff, sequence reset) | Who was connected when, in sequence with their orders |
Drop copies. A drop copy is a second, read-only stream of a member's own execution reports, across all its sessions, sent to the member's risk and back-office systems. Same binary format as execution reports, on a separate connection; it's how a member rebuilds its position after any failure of its own trading systems, and after our DR declaration.
The clearing contract. One message per trade on the post-trade stream (JSON on the stream; the clearing house's own format is produced by our clearing gateway):
json{ "trade_id": "P12-00000048211903-01", "trade_date": "2026-03-03", "partition": 12, "partition_seq": 48211903, "symbol": "XYZ", "price": "100.0400", "quantity": 200, "buy": { "member": "GSX", "order_id": "O-12-48211903", "clearing_firm": "CF-0417" }, "sell": { "member": "MSQ", "order_id": "O-12-48190554", "clearing_firm": "CF-0091" }, "executed_at_ns": 34200000412000 }
trade_id is built from the partition, the input's sequence number and the fill index, so every replay, every standby and the DR site produce the same ID for the same trade. Consumers treat it as an idempotency key.
The DR declaration procedure (a document, rehearsed with members):
- Confirm the primary site is lost or can't be trusted (site staff, the colocation provider, our own monitoring).
- The incident commander declares DR. Only a person can do this; no automation moves the market between sites.
- Fence the primary site at the bunker (step 3.3).
- DR engines take their missing tails from the bunker and verify the last checkpoint digests.
- Members log on at the DR site (keeping connectivity there is a membership requirement) in a pre-open state and reconcile using drop copies.
- Resting orders are cancelled (our policy), then trading reopens with an announcement.
R3.4 Design Evolution: The Rules of a Market
Step 3.1: Regulators Want to Replay March 3rd
The problem: three years from now, a regulator asks us to show exactly how the book in XYZ evolved between 10:14 and 10:21 on 2026-03-03, including every order, every cancel, and why one firm's order was filled before another's. Our journal from that day was on drives that have since been replaced, and the engine code has changed 40 times. What would you do?
Lifecycle, and why it doesn't fight the lock. A lifecycle rule moves segments to S3 Glacier Deep Archive after a year (Object Lock retention stays with the object version when its storage class changes) and a lifecycle rule with Expiration plus NoncurrentVersionExpiration removes versions once their 7-year retention has passed (Expiration alone would only add a delete marker on this versioned bucket). Before the retention date, S3 refuses to delete a locked version. Restores from Deep Archive take up to 12 hours (standard) or 48 hours (bulk), which is fine for a regulator's request that arrives with weeks of notice; the newest year stays in S3 Standard for quick answers.
Primitive: Event Sourcing and CQRS (the event log as the record; projections rebuilt from it)
Step 3.2: One Firm's Shorter Cable Gives It an Edge
The problem: Firm A's rack is 20 metres of cable from our gateway switch; Firm B's is 150 metres away. Light in fibre travels about 5 nanoseconds per metre, so A's orders arrive about 650 ns earlier than an identical order from B sent at the same instant. In a race to the same price, A wins almost every time. B has complained to the regulator. What would you do?
Step 3.3: The Primary Site Is Lost
The problem: the board wants no trade lost even if the whole primary building is lost. Today every released input is on two hosts, but both are in that building. The DR site is 500 km away. What would you do?
Latency against distance (release time = 10 + 5 + 2 + max(match, journal, standby, remote copy) + 10 µs, from the R2.5 budget):
| Where the second copy lives | Route distance | Round trip (≈ 10 µs/km + ~10 µs) | Release P99 budget |
|---|---|---|---|
| Another host, same site (Round 2) | < 100 m | ~15 µs (measured target) | 42 µs |
| Journal bunker (chosen) | ~10 km | ~110 µs | ~137 µs |
| A metro DR site | ~50 km | ~510 µs | ~537 µs |
| The far DR site | ~500 km | ~5,010 µs | ~5.04 ms |
Synthesizing vector architecture diagram...
Only the bunker is on the order path. The far site stays nearly current on its own and borrows the last few milliseconds from the bunker when it takes over.
Primitive: Cloud Disaster Recovery and Multi-Region Active-Active (RPO and RTO as designed numbers; synchronous only where required)
Step 3.4: Clearing Needs Every Trade Exactly Once
The problem: the clearing house needs every trade on trade date to settle on T+1. Today our post-trade servers write an end-of-day file. Last month the file was late by three hours after a disk problem, and last week a partner found a trade in the file twice. What would you do?
The trade store in the Region (Aurora PostgreSQL):
sqlCREATE TABLE cleared_trades ( trade_id TEXT PRIMARY KEY, -- 'P12-00000048211903-01'; a duplicate insert does nothing trade_date DATE NOT NULL, partition_no SMALLINT NOT NULL, partition_seq BIGINT NOT NULL, payload JSONB NOT NULL, status TEXT NOT NULL CHECK (status IN ('RECEIVED','SUBMITTED','ACCEPTED','REJECTED')), submitted_at TIMESTAMPTZ, accepted_at TIMESTAMPTZ ) PARTITION BY RANGE (trade_date);
Monthly partitions are detached and exported to S3 after two years (the journal archive can regenerate them anyway). Nothing references this table by foreign key, so retention never blocks.
Primitive: Change Data Capture and the Outbox Pattern (publish from the committed log, not from the application's memory)
Step 3.5: Can We Run in the Cloud?
The problem: the board asks: "Why do we still run data centers? Can the exchange run in AWS?" What would you do?
Step 3.6: Meme-Stock Day, 3× Volume
The problem: a meme-stock day triples the day's volume to 1.5 billion inputs, and one stock alone carries 15% of all messages. On the last such day, two partitions' rings passed 50% and members' orders were rejected with BUSY for minutes.
What would you do?
Round 3 Step Summary
| Step | Problem | Component | What it costs us |
|---|---|---|---|
| 3.1 | Replay March 3rd | Journal as the system of record; S3 Object Lock, 7 years; checkpoint digests; versioned engine builds | Storage; determinism forever |
| 3.2 | Shorter cables win | Equal-length cross-connects, equal gateway paths, simultaneous data; no speed bump | Coiled fibre, monitoring |
| 3.3 | The site is lost | Sync journal bunker at ~10 km, async far DR, fence at the bunker, human declaration | ~110 µs on every order; a halt if the bunker is unreachable |
| 3.4 | Trades to clearing exactly once | MSK post-trade stream, deterministic trade IDs, idempotent consumers, acceptance from the clearing house, three-way reconciliation | Two paths to reconcile |
| 3.5 | Cloud? | Core near members; post-trade, archive, replay, analytics and slow market data in the Region; Outposts bake-off | A hybrid architecture |
| 3.6 | 3× volume | Plan for 2× the highest peak, 32 partitions, pre-market load tests, moves between sessions, circuit breakers | Idle capacity on normal days |
R3.5 Global Architecture
Synthesizing vector architecture diagram...
The order path never leaves the primary site except for one synchronous hop to the bunker. Everything that can tolerate milliseconds (clearing, archive, replay, slow market data) runs in the Region, fed over Direct Connect.
Component counts: primary site 41 servers (16 gateways, 16 engine hosts as 8 primary and 8 standby for 32 partitions, 4 recovery servers, 3 arbiter nodes, 2 relay and post-trade servers); DR site the same 41; bunker 3 journal servers: 85 servers on-site. Direct Connect: two 10 Gbps dedicated connections from each site on separate devices, 4 in total.
Trace 1: a regulator replay.
Synthesizing vector architecture diagram...
The replay proves itself as it goes: every second's digest must equal the one the live engine published that day. A mismatch would stop the report, not produce a wrong one.
Trace 2: a site failover (the primary building loses power at 14:02; timings are drill targets):
| Time | Event |
|---|---|
| 0 | Primary site dark. The bunker holds every released input and maybe a few unreleased ones. DR followers are about 3 ms behind. |
| +5 s | Alarms at the DR site and the bunker: all primary heartbeats and the replication streams stopped. Nothing moves automatically. |
| +2 min | On-call confirms with the colocation provider; the incident commander is paged. |
| +15 min | DR declared. Bunker servers move to the new epoch (2 of 3 accept): the primary site is fenced even if it comes back. |
| +16 min | Each DR engine fetches its missing tail from the bunker and applies it; last checkpoint digests verified against the bunker's copy of the checkpoints. |
| +20 min | DR gateways accept logons in pre-open. Members receive drop copies from their last acknowledged message, including results of inputs the primary had sequenced but never released. |
| +45 min | All resting orders cancelled (policy), published on feeds and drop copies. |
| +60 min | Trading reopens. |
About an hour in drills, inside the 2-hour documented target. The 15 minutes before declaration is deliberate: moving a market between sites on a false alarm is worse than a few minutes of certainty.
Trace 3: a trade reaching clearing.
Synthesizing vector architecture diagram...
The trade is only "handed off" at the clearing house's acceptance. Every earlier hop can repeat safely because the trade ID is the same every time.
R3.6 Numbers and Cost
Extreme-day traffic.
Hosts. 32 partitions at 4 per host is 8 primary and 8 standby engine hosts: 16 per site. Gateways: we estimate one gateway host handles 100K messages a second (to be load-tested); , plus 3 spare = 16. Recovery servers keep a whole day's feed in memory, 78 GB on an extreme day (below), so they move from 64 GB to 128 GB machines.
Journal and archive. An extreme day is GB of journal. For a year we assume 240 normal days and 12 extreme ones:
The published market data is archived too, at GB on a normal day and 78 GB on an extreme one:
About 16 TB a year raw. We assume fixed-size binary records compress about 3× (to be measured): ~5.3 TB a year stored. Kept 7 years, the archive holds TB at steady state.
| Archive item | Math | Monthly |
|---|---|---|
| Newest year, S3 Standard | 5,340 GB × $0.023 | ≈ $123 |
| Six older years, Glacier Deep Archive | 32,040 GB × $0.00099 | ≈ $32 |
| A replica in a second Region (same classes) | the same again | ≈ $155 |
| Replication transfer | 5,340 GB a year × $0.02/GB ÷ 12 | ≈ $9 |
| Total | ≈ $320/month |
Object Lock adds no storage charge of its own. The audit archive of a national exchange costs less than one server.
Bandwidth to the bunker and the DR site at the extreme peak: MB/s ≈ 330 Mbps. Market data at that peak: MB/s ≈ 308 Mbps per feed. Both fit 10 Gbps links with room for millisecond bursts.
Replay. An extreme day is M inputs per partition: about 23 seconds per partition at 2M inputs/s, all partitions in parallel. The nightly verification replay is minutes of compute.
DR link latency is the table in step 3.3: 10 µs per route kilometre round trip, plus equipment. The bunker's ~110 µs is the single largest item in the new release budget:
| Stage | P99 target |
|---|---|
| Gateway | 10 µs |
| Sequencer | 5 µs |
| Ring handoff | 2 µs |
| Match ‖ journal ‖ standby ‖ bunker | max(8, 15, 15, 110) = 110 µs |
| Publish | 10 µs |
| Total | 137 µs, target P99 < 150 µs |
Cloud market-data egress. We assume 5,000 non-colocated subscribers at about 20 KB/s each (a conflated feed) during trading hours: 100 MB/s × 23,400 s × 21 days ≈ 49.1 TB a month. At list prices: 10,240 GB × $0.09 + 38,900 GB × $0.085 ≈ $4,230.
Monthly cost in the Region (us-east-1 list prices, 730 hours a month):
| Item | Math | Monthly |
|---|---|---|
| Direct Connect, 4 dedicated 10 Gbps ports (pay-as-you-go) | 4 × $2.25/h × 730 h | ≈ $6,570 |
| Direct Connect data out (acceptances, drop copies back to sites) | a few TB × $0.02/GB | ≈ $50 |
MSK, 3 × kafka.m7g.large + 3 TB storage | 3 × $0.204 × 730 ≈ $447; 3,000 GB × $0.10 = $300 | ≈ $750 |
Aurora trade store, 2 × db.r6g.2xlarge + storage + I/O | 2 × $1.038 × 730 ≈ $1,515; ~3 TB × $0.10 ≈ $300; ~3 billion I/Os × $0.20/M ≈ $600 | ≈ $2,420 |
Clearing gateway, 2 × c7i.xlarge | 2 × $0.1785 × 730 | ≈ $260 |
Market-data fan-out, 6 × c7i.2xlarge + NLB + egress | $1,564 + ~$50 + $4,230 | ≈ $5,840 |
| Replay farm, nightly | 32 × c7i.2xlarge × 1 h × 21 days × $0.357 | ≈ $240 |
| Audit archive | from above | ≈ $320 |
| Athena and analytics | an estimate | ≈ $200 |
| CloudWatch, logs, alarms | an estimate | ≈ $1,500 |
| Total | ≈ $18.2K/month |
On-site, rough and unverified. 85 servers ($2.2M), low-latency networks at two sites plus bunker equipment ($1M): about $3.2M, or ~$66K a month over 4 years. Add colocation space, power and cross-connects at three sites, and wavelength or dark-fibre circuits to the bunker and the DR site: very roughly $60K to $100K a month. About $130K to $170K a month. As a yardstick, just the 64 engine and gateway hosts as c7i.metal-24xl instances would list at about $200K a month, so owning is not the expensive option here; latency is the reason the core stays put, not price.
R3.7 Trade-Offs
| Choice | We chose | What we give up |
|---|---|---|
| RPO 0 across sites vs distance | A synchronous bunker at ~10 km, an async DR site at ~500 km | ~110 µs on every order, and a halt if the bunker is unreachable. A regional disaster can still lose the async window (normally ~3 ms, alarmed above 10 ms), which we document. Fully synchronous DR at 500 km would cost ~5 ms per order; async-only would lose released trades in a building fire. |
| Speed bumps vs raw speed | No speed bump; equal paths instead | A speed bump can blunt latency arbitrage against resting orders; we decided our members' complaint was about unequal paths, which equal cables fix without slowing everyone. |
| Cloud vs colocation vs hybrid | Hybrid: core near members on our hardware (Outposts evaluated at refresh); everything else in the Region | Two operating models and Direct Connect to run. All-cloud in a Region would add milliseconds for colocated members and lose multicast; all-colocation would keep audit, replay and analytics on hardware we have to buy for peaks. |
| Human vs automatic DR declaration | Human, with a rehearsed procedure | Up to ~15 minutes of certainty-seeking. An automatic cross-site switch on a network blip could start a second market. |
| Cancel resting orders at DR vs keep them | Cancel | Members must re-enter orders; in exchange, nobody has an order live that their risk systems lost track of during an hour-long outage. |
Closing the loop. The opening question was: how do we match orders fairly, deterministically and fast, and never lose one? The answer: one order of events (a sequencer per partition, and equal paths to it), same inputs, same outputs (one writer per book, no clocks or randomness inside the engine, verified by digests every second and replayable years later with the same build), and nothing released is lost (release only after the input is on the standby, the local journal and the bunker). Speed came from the same choices: no locks, no allocation, no waiting on anyone who doesn't need to be waited on. The one place we deliberately made it slower, the bunker, is where the board chose safety over microseconds, with the number in front of it.
R3.8 Failure Modes
| Failure | What you'd see | How the design responds |
|---|---|---|
| Primary site lost | All primary heartbeats stop; DR and bunker alarm | Human declaration, fence at the bunker, DR takes its tail from the bunker, members reconcile with drop copies, reopen (Trace 2). No released trade lost. |
| Bunker unreachable | Bunker acknowledgments stop on all partitions | Releases stop after 50 ms: in effect, a market halt. Operators restore a fibre route or, after confirming no DR declaration is under way, formally declare "bunker lost" and move to a documented degraded mode with a regulator notification. |
| Determinism bug (primary and standby digests differ) | A CHECKPOINT mismatch on one partition | Halt the partition (a CONTROL input). Replay the journal to that checkpoint on a separate machine: whichever engine matches the replay is right. If the bug is in the engine build, both may diverge from the replay at the same input: fix, test by replaying the day, and ship only between sessions. Treat as a P1 incident, with a regulator notification if trading was affected. |
| Clearing stream outage | MSK unreachable or the clearing gateway down; SUBMITTED without ACCEPTED ages | Trading continues: post-trade is off the hot path. Publishers stop and resume from their read positions; duplicates are no-ops. If the outage runs past the clearing house's cut-off, the end-of-day file (regenerated from the journal) is the fallback. |
| An extreme day beyond capacity | Ring fill above 50% on several partitions; BUSY rejections climbing | Backpressure protects the engine; LULD pauses slow runaway stocks; market-wide circuit breakers halt everyone if the index falls far enough. Tomorrow's partition map isolates the stocks that caused it. |
| Clock problem at the sequencer | Offset from UTC above our alarm threshold | Timestamps are for audit and for timer-based rules, not for ordering (the sequence decides order). We alarm on the offset, fail over to the partition's standby sequencer if it's the host's clock, and record the offset for the audit trail. Regulators set clock-synchronization tolerances for venues; we stay well inside them with PTP-disciplined clocks. |
R3.9 Runbook and Incident Response
| Signal | Alarm | Severity | First action |
|---|---|---|---|
| Wire-to-wire latency, P99 and P99.9, per partition (µs) | P99 > 150 µs for 1 min, or P99.9 > 500 µs | P2 | Check ring fill and bunker round trip for that partition; check core isolation on the host |
| Input ring fill | > 25% (backpressure starts) | P2; > 50% P1 | Find the hot symbol; consider a pause if it's a runaway stock; plan tomorrow's move |
| Sequence gaps: members' sessions, gateway to sequencer | Rejections > 50/s from one member, or any gateway gap | P2 | Check that member's line or our gateway's NIC drop counters |
| Standby lag (acknowledged vs sequenced) | > 1 ms | P2 | Check the standby host and replication link |
| Bunker round trip | > 200 µs P99, or one of three servers silent | P1 | Check both fibre routes; prepare for a halt if a second server goes silent |
| DR follower lag | > 10 ms | P2 | A lagging DR site means a larger window in a regional disaster |
CHECKPOINT digest mismatch | any | P1 | Halt the partition; start the verification replay |
| Market-data retransmission requests | > 500/s on a channel | P2 | Check drops on feeds A and B; a spike on both points at our publisher |
| LULD pauses and market-wide halts | any | Info | Confirm our trading status messages match the plan's |
Clearing: SUBMITTED without ACCEPTED | older than 15 min | P2 | Check the clearing gateway and the clearing house's status |
Opening procedure (every trading day)
- Load and verify the reference file: symbols, partition map, tick sizes, LULD tiers, collars.
- Start engines with the day's build on primary, standby, bunker and DR; confirm build hashes match on all.
- Confirm the arbiter holds a primary for every partition and the bunker acknowledges on all partitions.
- If a heavy day is expected, the DR engines ran the 2× replay of a recorded extreme open before step 2 and were restarted clean.
- Open sessions at 07:00 for logon; books start empty; trading starts at 09:30 with the first
CONTROLinput.
Closing procedure: at 16:00 a CONTROL input ends trading; DAY orders expire (published); final CHECKPOINT; journals sealed, hashed and uploaded to S3; nightly verification replay compares every checkpoint; three-way clearing reconciliation.
Failover within the site is automatic (step 2.4). The runbook covers what follows: confirm the new primary's digest matches the next checkpoint, rebuild the failed host as the standby, and file the incident.
Halting a symbol or the market: issue a CONTROL input (halt, with reason and ticket); it is sequenced, journaled, replayed like any other input, and published as a Trading Status message. Resume the same way. Never halt by stopping processes: an input is the only way that the halt is in the record.
Go deeper: CLI playbook
Plain AWS commands an on-call engineer runs, one at a time. Replace the names with real ones.
text# 1. Alarms currently firing for the exchange aws cloudwatch describe-alarms --state-value ALARM --alarm-name-prefix exchange- # 2. State of the Direct Connect links from both sites aws directconnect describe-connections # 3. Confirm the audit bucket's default Object Lock rule aws s3api get-object-lock-configuration --bucket exchange-journal-archive # 4. Retention on one journal segment aws s3api get-object-retention --bucket exchange-journal-archive --key journal/2026-03-03/p12/000417.seg # 5. Start restoring an archived segment for a regulator replay (Bulk: within 48 hours) aws s3api restore-object --bucket exchange-journal-archive --key journal/2026-03-03/p12/000417.seg --restore-request '{"Days":7,"GlacierJobParameters":{"Tier":"Bulk"}}' # 6. Check whether the restore has finished (see the Restore field) aws s3api head-object --bucket exchange-journal-archive --key journal/2026-03-03/p12/000417.seg # 7. State of the post-trade MSK cluster aws kafka list-clusters-v2
R3.10 Pillar Check
| Pillar | What Round 3 adds |
|---|---|
| Reliability | Synchronous bunker so a site loss loses no released trade; async far DR with a documented residual RPO; a fenced, human-declared, rehearsed site failover (~1 h in drills, 2 h documented); 32 partitions with headroom at 2× the highest peak REL 9 · REL 12 · REL 13 |
| Security | Immutable audit archive (Object Lock compliance mode) with hash-chained segments; operator actions only by named operators with change tickets, each one a journaled input; nobody, including administrators, can delete a locked segment early SEC 3 · SEC 4 · SEC 8 |
| Performance Efficiency | 137 µs P99 budget with the bunker, derived from distance; the core stays near members; slow consumers moved to cloud fan-out PERF 1 · PERF 4 |
| Cost Optimization | ~$18.2K/month in the Region, ~$320 of it for a 7-year archive; on-site ~$130K to $170K (rough); the Outposts decision made by bake-off, not by slogan COST 5 · COST 11 |
| Operational Excellence | Compliance requirements drive the design (audit, retention, DR resumption); golden signals in µs; opening, closing, halt and DR procedures; builds change only between sessions OPS 1 · OPS 6 · OPS 10 |
| Sustainability | Audit data moves to archive storage after a year; the replay farm runs only at night; idle headroom partitions share hosts; busy-polling cores limited to the pipelines that need them SUS 2 · SUS 4 |
R3.11 Round 3 Rubric and Follow-Ups
What an architect (L7) answer adds over L6
- Treats the journal as the legal record: immutable, retained, and replayable with the exact engine build, proven by checkpoints.
- Puts a number on RPO 0 across sites (10 µs per route kilometre, round trip) and designs the bunker plus far-DR pattern to get zero loss without a 5 ms penalty.
- Makes the fence hold during a declaration, and refuses automatic degradation that would create a second market.
- Designs fairness into the physical layout, and has a reasoned position on speed bumps.
- Hands trades to clearing exactly once in effect, confirmed by the clearing house's acceptance, and reconciled three ways.
- Answers "can we run in the cloud?" component by component, with accurate AWS facts (multicast, ENA Express, Outposts), and knows a real example.
- Plans capacity from history with headroom, and knows which limits (one symbol, one core) can't be bought away.
Follow-up questions
-
"Why not put the bunker 1 km away and save 90 µs?" Answer: the bunker exists to survive whatever destroys the primary building: fire, flood, a power or network event in that neighbourhood. At 1 km it may share the same substation, flood plain or fibre ducts. We pick the closest site that shares none of those, and ~10 km is our estimate of that; the latency follows from the choice, not the other way round.
-
"Two years from now, a new engine build changes how IOC orders round. Can we still replay today?" Answer: yes, because we replay each day with the build that ran that day, which we keep for the whole retention period. We never "replay old days with new code" for audit; that would answer a different question.
-
"The board asks you to cut the latency back to 50 µs." Answer: then the board is choosing between two losses: it can move the bunker's copy into the same building (and lose released trades if the building is lost) or accept documented async DR. We'd present both with numbers, not decide it quietly.
Interview gotchas from this round's wrong answers
| Gotcha | Why it's wrong |
|---|---|
| "Keep logs" for the audit trail | Logs aren't complete, immutable or replayable. |
| "Synchronous replication to the DR site" at 500 km | About 5 ms on every order. |
| "Automatically fail over between sites" | A network blip can start a second market; declarations are human. |
| "Auto-degrade if the bunker is unreachable" | A degraded primary during a DR declaration is an unfenced second market. |
| "Stream trades straight from the engine to clearing" | A partner's slowness backs up into the matching core. |
| "Run the engines on regular instances in a Region" | Milliseconds from members, no multicast, shared hosts. |
| "ENA Express makes it faster" | It raises single-flow bandwidth and cuts tail latency under congestion; it can add median latency. |
| "Size for the worst day ever" | The next one is worse, and a single symbol can't be split anyway. |
Loop Closer: Interview Strategy for All Three Rounds
How to Run Each 60-Minute Round
| Time | Round 1 | Round 2 | Round 3 |
|---|---|---|---|
| 0–5 min | Scoping: order types, priority rule, instruments, latency, risk checks | Restate the Round 1 design in 60 seconds | Restate the Round 2 design in 60 seconds |
| 5–15 min | Requirements + API (client order ID, integer ticks, sequence numbers) | Scope raise → what breaks | Scope raise → what breaks |
| 15–40 min | Steps 1.0–1.6: book → one writer → sequencer → journal and replay → market data → risk | Steps 2.1–2.7: no allocation, rings, durability, lockstep standby, feeds, bands, partitions | Steps 3.1–3.6: audit, fairness, bunker and DR, clearing, cloud, extreme days |
| 40–50 min | Numbers, latency with max() for parallel steps, cost | Latency budget, rate reconciliation, memory, feed bandwidth | Distance vs latency, archive, cloud split, cost |
| 50–60 min | Failures + pillar check | Failures + pillar check | Failures, runbook, pillar check |
For how to spend a single 45-minute round, see the 45-minute interview blueprint.
The Two Sentences That Matter Most
- Opening a round: "An exchange sells fairness, so before speed I'll fix what 'first' means: one sequencer gives every input one number, and one thread per book applies them in that order, which also makes the whole thing replayable."
- When anything fails: "We never release anything about an input until it's stored on another machine, so a failover can only lose things nobody was told about, and the new primary recomputes the rest identically."
Well-Architected Review Sheet
Interviewers rarely ask "which pillar is this?". They ask the pillar's question in plain words. Rehearse one sentence per row.
| Pillar | Question you'll hear | One-sentence answer | Round | Backed by |
|---|---|---|---|---|
| Reliability | "What if the engine crashes?" (REL 11) | Round 1 replays the quorum journal from a snapshot in ~20 s; Round 2's lockstep standby takes over in ~5 ms (target). | 1–2 | Step 1.4, step 2.4 |
| "How do you stop two primaries?" (REL 11) | An arbiter's epochs, and a standby that stops acknowledging before it asks to be promoted, so the old primary can't release anything. | 2 | Step 2.4 | |
| "What if a burst overloads you?" (REL 5) | Bounded rings; gateways push back at 25% and reject new orders (not cancels) at 50%. | 2 | Step 2.7 | |
| "What if the building burns?" (REL 13) | A synchronous bunker 10 km away holds every released input; the far DR site takes its tail and reopens within the documented 2 hours. | 3 | Step 3.3, R3.5 | |
| "How do you know failover works?" (REL 12) | Failover and DR drills with members, timed against targets, and nightly verification replays. | 2–3 | R2.5, R3.9 | |
| Security | "Who can send orders, and how much?" (SEC 2) | Authenticated sessions, credit and fat-finger checks before sequencing, collars and LULD bands in the engine. | 1–2 | Step 1.6, step 2.6 |
| "Could someone alter the audit trail?" (SEC 4) | Not undetected: hash-chained segments in S3 Object Lock compliance mode, and operator actions are journaled inputs. | 3 | Step 3.1 | |
| Performance | "Where do the microseconds go?" (PERF 1) | 42 µs budget with match, journal and standby in parallel; 137 µs once the bunker joins. | 2–3 | R2.5, R3.6 |
| "Why one thread per book?" (PERF 2) | Orders on one book are sequential anyway; one writer removes locks and makes results deterministic. | 1–2 | Step 1.2, step 2.2 | |
| "Why not just run it in a Region?" (PERF 4) | Members are in the colocation site, a Region is milliseconds away, and VPCs have no native multicast. | 2–3 | Step 2.5, step 3.5 | |
| Cost | "What does it cost?" (COST 5) | About $2.4K a month in Round 1; ~$40K owned ($58.6K as an EC2 metal yardstick) in Round 2; ~$18K in the Region plus ~$150K on-site (rough) in Round 3. | 1–3 | R1.7, R2.6, R3.6 |
| "Why multicast?" (COST 8) | One send per feed instead of a thousand: ~103 Mbps instead of ~103 Gbps at the busiest second. | 2 | R2.6 | |
| Operations | "How do you run the day?" (OPS 10) | Opening and closing procedures, halts as sequenced inputs, human DR declaration, golden signals in microseconds. | 3 | R3.9 |
| "What regulations shape this?" (OPS 1) | Audit and retention, price bands and circuit breakers, T+1 settlement, and a documented DR resumption target. | 2–3 | Step 2.6, R3.1 | |
| Sustainability | "Where is the footprint?" (SUS 4) | Busy-polling cores all day, and 7 years of archive; archive moves to deep storage after a year, and only pipeline cores are pinned. | 2–3 | R2.10, R3.6 |
Rubric Across Levels
| Dimension | L5 (Round 1) | L6 (Round 2) | L7 (Round 3) |
|---|---|---|---|
| Order book | Sorted levels, FIFO queues, O(1) cancel index, trade at the resting price | Price-indexed arrays, 64-byte nodes in preallocated pools | Capacity from history; hot symbols isolated between sessions |
| Ordering and fairness | Sequencer, not timestamps | Session sequence checks; one sequence per partition | Equal-length paths; a reasoned speed-bump policy |
| Determinism | One writer per book; journal and replay | Lockstep standby, no clocks in the engine, timer inputs, digests | Bit-identical replay years later with the recorded build |
| Durability | Release after a 2-of-3 quorum across AZs; epoch fencing | Release after local journal + standby; fence by withheld acknowledgments | Synchronous bunker, async far DR, fence at the bunker, human declaration |
| Market data | Sequenced stream, gap-fill, snapshots, slow readers dropped | Multicast A/B, arbitration, retransmission and snapshot servers | Cloud fan-out for non-latency-sensitive subscribers; equal delivery on-site |
| Well-Architected trade-offs | Latency budget with max() for parallel steps; derived cost | Rate reconciliation per scope; multicast vs unicast bandwidth | RPO 0 vs distance; cloud vs colocation vs hybrid, piece by piece |
| Evolving under new scope | Builds from a SQL table one problem at a time | Opens with "what breaks", keeps determinism while going about 70× faster | Changes the system's physical shape (sites, Region) without weakening the three rules |