System Design Interviews
Design an exchange, a feed handler, an OMS — out loud.
Mastery · Markets, Strategies & Getting Hired
Why this week
Trading-infrastructure system design interviews aren't the web-scale kind. Nobody asks you to shard a URL shortener across ten thousand machines. They ask you to design a matching engine, a feed handler or an order management system that runs on a handful of tuned hosts, and then they push on the numbers: how many microseconds is that hop? What happens to your book when you lose a packet? Why is the engine single-threaded? What if the primary dies between the fill and the ack? A strong answer is specific, quantitative and honest about trade-offs. A weak one names technologies.
You have an unfair advantage: you've built every one of these systems over the past 48 weeks and measured them. This week turns that into the ability to design them out loud, in 45 minutes, for someone who's deciding whether to hire you. Practise against the site's AI system-design interviewer, which plays the interviewer and pushes back on vague answers, and keep your estimation reflexes sharp with the quick drills: latency-number estimation, memory-ordering litmus tests and spot-the-concurrency-bug.
Interviewers grade your process as much as your design. Restate requirements as numbers, budget latency before you draw boxes, name a failure mode for every box, and say which trade-off you made and what it cost. Silence while you think is fine. Silence instead of a number isn't.
Day 1 — The shape of a strong answer
A framework you can run under pressure
Requirements → numbers → budget → components → failures → trade-offs
Use the same seven steps every time, so your working memory goes to the problem and not the structure:
- Requirements, as numbers. Functional scope (which order types, how many instruments), the latency target as a percentile and where it's measured (wire-to-wire? gateway-in to ack-out?), peak versus average throughput, fan-out, and correctness requirements: ordering, determinism, recovery point and recovery time, audit.
- Back-of-the-envelope. Bytes per message, messages per second at peak, bandwidth, memory for the state, cores needed with headroom. Two minutes of arithmetic here prevents twenty minutes of wrong design.
- Latency budget. Split the end-to-end target across stages before choosing components. If the network hop alone eats 5 µs of a 10 µs budget, that decides the design.
- Components and threading. Which thread owns which state, what crosses each boundary (queue? shared memory? TCP?), where sequencing happens.
- Failure modes. For each component: what if it's slow, dead, or wrong? Detection, mitigation, and what clients see.
- Trade-offs. What you chose, the alternative, and the price: latency versus durability, simplicity versus throughput, fairness versus speed.
- Evolution and measurement. How you'd know it works (which histograms, which invariants) and what changes at 10× load.
Starting values are illustrative for a kernel-bypass setup. The point: a budget only helps if every stage has an owner and a number.
Replication, the trouble with distributed systems, and consistency and consensus. You don't need consensus on the hot path, and saying why (single sequencer, primary/standby with leases, deterministic replay) is a strong interview move. These chapters give you the vocabulary to say it precisely.
The origin of 'latency numbers every programmer should know' and of doing back-of-the-envelope calculations before designing. The specific numbers have aged, so update them with your own measurements from weeks 4, 21 and 31. The habit hasn't aged.
An interviewer says 'it needs to be fast — say 2 µs'. What do you ask before designing anything, and why does it change the design?
Day 2 — Design a market-data system
Feed in, books out, fifty consumers
The most common trading-infra design question
The prompt: "An exchange feed peaks at 3 million messages per second, 40 bytes on average, on a 10 Gb/s link. Normalise it into books and fan it out to 50 strategy processes on the same host. p99 from NIC to strategy must be under 10 µs."
A good answer, compressed:
- Ingress. Subscribe to both the A and B multicast lines, each on its own NIC queue. Use kernel bypass (ef_vi, DPDK or AF_XDP) or a tuned kernel path with busy polling. A pinned feed-handler thread per line, or one thread arbitrating both, sits on the NIC's NUMA node.
- Arbitration and recovery. First copy of each sequence number wins. Gaps go to retransmission, or to a snapshot when they're large, and the book is marked stale while recovering (weeks 46 and 48).
- Book building. Decode zero-copy (week 11), update a cache-friendly book (week 28), one writer thread per book.
- Distribution. A single-writer broadcast ring in shared memory (week 16) for the full update stream, plus a seqlock'd top-of-book per instrument (week 34) for consumers that only want the latest state. No locks, no syscalls per message, and every consumer reads at its own pace.
- Slow consumers. The writer never waits. A consumer that falls more than a ring's worth behind detects the overrun from sequence numbers and resynchronises from the seqlock'd state.
- Budget. NIC to user space ~1–3 µs with bypass (substantially more through the kernel), decode and book update a few hundred ns, publish into the ring ~100 ns, and the cache-line transfer to a consumer on the same socket ~100 ns. That leaves headroom for the tail, which is the point.
- Capacity. 3 M × 40 B × 8 ≈ 1 Gb/s of payload: a 10 Gb/s link is about 10% full on average, but microbursts at line rate are what size your rings.
The drill makes the rubric explicit. You write critique(), the reviewer that catches the classic weak-answer patterns, and then write your own outline for the prompt above and make it pass.
Outline a market-data system design
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Failure {
FeedGap, // packet loss / sequence gap on the inbound feed
SlowConsumer, // one consumer can't keep up
PublisherCrash, // the normalise-and-fan-out process dies
LinkSaturation, // a burst exceeds what the link or NIC can carry
}
#[derive(Debug, Clone)]
pub struct Stage {
pub name: &'static str,
pub budget_ns: u64, // this stage's share of the p99 budget; 0 = not stated
pub replicas: u32, // independent instances that can carry the load
}
#[derive(Debug, Clone)]
pub struct Outline {
pub p99_target_ns: u64, // end-to-end p99 target; 0 = not stated
pub peak_msgs_per_sec: u64, // peak inbound rate; 0 = not stated
pub msg_bytes: u64, // average message size
pub link_bits_per_sec: u64, // inbound link capacity
pub consumers: u32,
pub stages: Vec<Stage>, // the critical path, in order
pub handled: Vec<Failure>, // failure modes with a stated detection + mitigation
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Issue {
NoLatencyTarget, // p99_target_ns == 0
NoThroughputTarget, // peak_msgs_per_sec == 0
UnbudgetedStage(&'static str), // a stage with budget_ns == 0
OverBudget { total_ns: u64, target_ns: u64 }, // target stated and Σ budgets > target
SinglePointOfFailure(&'static str), // a stage with replicas < 2
LinkOverHalfFull, // peak stated and peak × bytes × 8 > link / 2
Unhandled(Failure), // each Failure variant missing from `handled`
}
/// Every issue in `o`, each once, in any order.
pub fn critique(o: &Outline) -> Vec<Issue> {
let _ = o;
todo!()
}
/// Your design for: an exchange feed peaking at 3,000,000 msgs/s of 40-byte
/// messages on a 10 Gb/s link, normalised and fanned out to 50 strategy
/// processes on the same host, p99 NIC→strategy <= 10 µs (10_000 ns).
/// Name your stages and budget them; critique(&my_outline()) must be empty.
pub fn my_outline() -> Outline {
todo!()
}
In your market-data design, one consumer needs every update (a recorder) and forty-nine only need the latest book. Defend one distribution mechanism for both, or two separate ones. What does each choice cost in latency, memory and failure isolation?
Day 3 — Capacity math without a calculator
The arithmetic interviewers expect you to do live
Framing, line rate, serialisation, batching, cores, Little's law
A handful of formulas cover most of what you'll be asked:
- Ethernet framing. A UDP/IPv4 packet carries 14 + 20 + 8 bytes of headers plus a 4-byte FCS, padded to a 64-byte minimum frame, plus 8 bytes of preamble and a 12-byte inter-frame gap on the wire. A minimum-size frame therefore occupies 84 bytes = 672 bits, and 10 GbE tops out at 14.88 million packets per second. Small messages are dominated by overhead, which is why feeds pack many messages per packet.
- Serialisation delay. A full 1,500-byte IP packet takes ~1.2 µs to serialise at 10 Gb/s and ~12 µs at 1 Gb/s. That's paid at every store-and-forward hop, which is one reason trading networks use cut-through switches and faster links even when bandwidth isn't the bottleneck.
- Microbursts. Average rates hide bursts at line rate lasting microseconds to milliseconds. Size rings, socket buffers and NIC queues for the burst, not the average.
- Cores.
messages/s × ns/messageis CPU-nanoseconds per second. Divide by 10⁹ for cores at 100%, then by your utilisation ceiling, because queueing delay explodes as a core approaches 100% busy. - Little's law. Items in flight = arrival rate × time in system. At 2 M msgs/s and 50 µs end to end, 100 messages are in flight on average. That number sizes buffers and tells you how many orders a crash can strand.
Dot = typical modern x86 server; band = plausible range across hardware. Click a row for details.
Practise these until they're automatic. The quick drills include a latency-number estimation round. Aim to be within a factor of two without looking anything up.
Back-of-the-envelope capacity math
/// Bits on the wire for one UDP/IPv4 packet carrying `payload` bytes over
/// Ethernet: Ethernet header 14 + IPv4 header 20 + UDP header 8 + payload +
/// FCS 4, padded up to the 64-byte minimum frame, plus 8 bytes of
/// preamble/SFD and a 12-byte inter-frame gap.
pub fn wire_bits(payload: u64) -> u64 {
let _ = payload;
todo!()
}
/// Packets per second the link can carry at line rate (round down).
pub fn line_rate_pps(link_bps: u64, payload: u64) -> u64 {
let _ = (link_bps, payload);
todo!()
}
/// Nanoseconds to serialise one such packet onto the link (round up).
pub fn serialization_ns(link_bps: u64, payload: u64) -> u64 {
let _ = (link_bps, payload);
todo!()
}
/// Messages per second at line rate when every packet carries a
/// `pkt_header`-byte feed header plus `per_pkt` messages of `msg` bytes:
/// full packets per second × per_pkt.
pub fn line_rate_msgs(link_bps: u64, pkt_header: u64, msg: u64, per_pkt: u64) -> u64 {
let _ = (link_bps, pkt_header, msg, per_pkt);
todo!()
}
/// Cores needed to process `msgs_per_sec` at `ns_per_msg` each without any
/// core above `max_util_pct` percent busy (round up; 0 if there's no load).
pub fn cores_needed(msgs_per_sec: u64, ns_per_msg: u64, max_util_pct: u64) -> u64 {
let _ = (msgs_per_sec, ns_per_msg, max_util_pct);
todo!()
}
/// Little's law: average messages in flight = arrival rate × latency (round up).
pub fn in_flight(arrivals_per_sec: u64, latency_ns: u64) -> u64 {
let _ = (arrivals_per_sec, latency_ns);
todo!()
}Without a calculator: roughly how many 40-byte messages per second fit on 10 GbE if you send one per packet, and why does packing 30 per packet roughly triple that?
Day 4 — Design an exchange
The question you have the best answer to
Sequencer, single-threaded matching, replicated journal
"Design a stock exchange" is the question where your year shows. The structure your answer should follow is the one you built:
- Gateways terminate sessions (FIX or binary), validate, throttle and run pre-trade risk. They scale horizontally and hold no book state.
- A sequencer assigns one total order to every inbound command. This is where fairness is defined: first sequenced, first matched.
- The matching engine is single-threaded per instrument, or per shard of instruments: a deterministic state machine over the sequenced stream. Single-threaded because price-time priority is inherently sequential per book, and because determinism makes replication and replay trivial. Throughput comes from mechanical sympathy and sharding, not locks.
- The journal and replication. The sequenced input is written to a journal and replicated to a hot standby that runs the same deterministic engine. Acknowledge after replication. Fail over on lease expiry with exactly one leader.
- Market data is generated from engine output events: sequenced L2/L3 incrementals, snapshots for recovery, A/B multicast.
- Drop copies and reporting consume the same event stream off the hot path.
Expect the follow-ups: How do you guarantee no double fill on failover? (Deterministic replay of the replicated sequence, plus idempotent client-visible ids.) Why not a database? (Fsync on every order costs tens of µs to ms. Replication to memory on a standby is faster, and the journal is still written for durability.) How do you add instruments? (Shard by symbol, one sequencer per partition, week 36.) What about cross-instrument orders? (They're hard. Say so, and describe the coordination cost.)
Business logic on a single thread fed by the Disruptor, with input journaling and replication handled by separate consumers. Map every part of it onto your own exchange and note where you made a different choice.
An exchange architecture from someone who has run real ones: sequencers, replicated state machines, and why the design is simpler than people expect. Use it to calibrate how deep an interviewer will go.
A general-audience treatment of the same question. Read it critically: note where its latency figures and component choices would be challenged in a trading-firm interview, and why.
An interviewer asks: 'Why is your matching engine single-threaded? Wouldn't multiple threads be faster?' Give the answer in three sentences.
Day 5 — Design an OMS or order router
The other side of the wire
State machines, races and reconciliation
The buy-side question is usually "design an order management system" or "a smart order router". The heart of a good answer is the order state machine and the races around it:
- States: pending-new → new (acked) → partially filled → filled, plus pending-cancel, pending-replace, cancelled, rejected and expired. Every transition is driven by an execution report, and you must decide what to do with reports that arrive "impossibly". A fill can arrive after you've sent a cancel, because the cancel crossed it on the wire. That's not an error. The venue says "too late to cancel", and your position must reflect the fill.
- Positions and risk across venues. The router splits a parent order into child orders on several venues, so the open quantity across all of them counts against limits (week 15). A fast fill on venue A must reduce what you're willing to send to venue B.
- Identity. Client order ids for every child, mapping back to the parent. Make cancel/replace chains explicit.
- Recovery and reconciliation. After a disconnect, resynchronise from sequence numbers (FIX resend, week 14), and reconcile against the venue's drop copy at the end of the day. A position that disagrees with the venue is an incident.
- Latency. Routing decisions use the freshest book you have, and the latency to each venue is part of the decision.
Your OMS sends a cancel for a resting order; the next message from the venue is a full fill for that order, then a 'cancel rejected: too late to cancel'. What state is the order in, and what bug do naive OMSs have here?
Design a smart order router for one stock traded on three venues with different fees and latencies (40 µs, 120 µs, 300 µs). How do you split a 10,000-share marketable order, what do you do when the fast venue fills and the slow one hasn't acknowledged, and how do you avoid trading through your own stale view?
Day 6 — Mock interviews
Practise the performance, not just the design
Timed, recorded, reviewed
Run at least three timed sessions with the AI system-design interviewer: one exchange, one market-data system, one OMS. Treat them like the real thing: 45 minutes, no notes, talk continuously, draw as you go. Afterwards, review the transcript against the seven-step framework. Where did you name a technology instead of a mechanism? Where did you skip a number? Which failure mode did the interviewer have to drag out of you?
The common failure patterns are predictable: designing before clarifying, a budget that doesn't add up, a single component with no failure story, "Kafka" on a microsecond path, and running out of time before failure modes. Fix one per session.
After your first mock: list the three moments you were weakest, what you said, and what you'll say next time. Then pick the one number you didn't know and measure it on your own exchange.
Capstone
Three designs, said out loud
Three timed design write-ups and three reviewed mock interviews, each with numbers, a latency budget and failure modes.
- Exchange / matching engine design, 45 minutes, written up#exchange-designmark when done
Set a 45-minute timer and design an exchange for a single equity market (1,000 instruments, 500 k orders/s peak, p99 gateway-in to ack-out of 50 µs). Then write it up in two pages: requirements as numbers, back-of-the-envelope, the budget per stage, components and threads, failover without double fills, and three trade-offs with their costs. Use your own measured numbers wherever you can.
- Market-data distribution design with capacity math#md-designmark when done
Your Day 2 design as a one-page write-up with a diagram: ingress, arbitration, recovery, book building, distribution, slow consumers. Include the capacity math from Day 3 (bandwidth, packets per second, cores with headroom, ring sizes from Little's law and burst assumptions).
- OMS / order router design with the order state machine#oms-designmark when done
A state diagram covering every order state and transition, including the cancel/fill and replace/fill races, plus the cross-venue position and risk logic and how reconciliation catches mistakes.
- Three mock interviews with the AI interviewer, reviewed#mock-interviewsmark when done
Three sessions at /hft/interview on three different prompts. For each, a short review: weakest moment, one number you lacked and then measured, and one change for the next session.
- Latency-number estimation drills until they're reflexes#estimationmark when done
Repeat the latency-number estimation round at /hft/drills until you're consistently within 2× on every item, then do one memory-ordering litmus round and one spot-the-concurrency-bug round to warm up for week 50.
Feeds into
Nothing later depends on this week formally, but it pairs with week 50, which goes deep on the technical questions that follow a design round, and week 51, where the exchange design you wrote up becomes the architecture overview in your README.