- "Design Zerodha" and "design NSE" are different systems. The broker handles users, risk checks, order routing and price streaming; the exchange runs the order book and matching engine. Clarify which, then cover both at the right depth.
- The order book: price levels sorted per side, FIFO queues per level, a hash map for O(1) cancels. Matching is price-time priority, trading at the resting order's price.
- Exchanges run a single-threaded matching engine per instrument partition, fed by a sequencer that stamps every input with a sequence number and journals it. Replicas replay the same log to stay identical: event sourcing at microsecond scale.
- Low latency comes from no locks, no GC pauses, pre-allocated memory, CPU pinning, kernel bypass networking and colocation, not from adding servers.
- Market data is a huge fan-out: exchanges multicast, brokers conflate and push over WebSockets per subscribed instrument, with a big spike at the 9:15 AM open.
Trading systems are a favourite at fintech companies because they invert the usual system design instincts. Horizontal scaling, eventual consistency and "just add a cache" don't help a matching engine, where every order must be processed in a single, deterministic sequence within microseconds. At the same time, the broker side is a classic high-scale consumer backend with a brutal daily spike at market open. This guide covers both, with a working order book you can run.
The Broker vs Exchange Split
| Broker | Exchange | |
|---|---|---|
| Users | Millions of retail investors | Hundreds of member firms (brokers, trading firms) |
| Key latency | Milliseconds (app → exchange round trip) | Microseconds inside the matching engine |
| Scaling style | Horizontal, stateless services, caches | Vertical per partition, single-threaded hot path |
| Hard problems | Risk checks, 9:15 AM spike, price streaming to millions | Determinism, fairness, sequencing, failover without losing an order |
| Regulated by | SEBI and the exchanges | SEBI |
Life of an Order
- User places "BUY 10 RELIANCE LIMIT ₹2,500.50" in the app.
- Broker OMS validates (instrument, lot size, tick size, price band, market hours), assigns an order ID, persists it as
PENDING. - Broker RMS checks margin/funds (₹25,005 plus charges available?), holdings for sells, and per-client limits; blocks the margin.
- Exchange gateway sends the order to the exchange over the member connection, respecting the member's message rate limits.
- Exchange validates, the sequencer assigns a sequence number and journals it, the matching engine either matches it or rests it in the book, and sends an acknowledgement (
OPEN) and any trade confirmations. - Broker updates the order (
OPEN→PARTIALLY_FILLED→COMPLETE), positions, and pushes updates to the app. - Post-trade: the clearing corporation becomes the counterparty, and settlement moves shares and money. India moved its equity cash market to T+1 settlement in 2023.
Requirements
Exchange
- Accept new, modify and cancel orders for thousands of instruments (equities, futures, options).
- Order types: limit, market, stop-loss (trigger price), IOC and day validity; pre-open call auction at the open.
- Match with price-time priority; publish trades and market data (top of book, depth, last traded price).
- Enforce price bands/circuit limits; halt instruments when required.
- Non-functional: deterministic, fair (first come, first served by sequence), microsecond-level matching latency, zero lost orders, fast failover, full audit trail.
Broker
- Login with 2FA, place/modify/cancel orders, order book and trade book, positions, holdings, funds.
- Real-time quotes and market depth for watchlists; charts; GTT (good-till-triggered) orders and price alerts.
- Non-functional: survive the market-open spike, accurate margin checks, order status consistent with the exchange.
Estimates
Public figures for Indian exchanges and brokers vary year to year and are not broken down at this granularity, so use clearly labelled assumptions:
Exchange (assumed)
- Peak order messages (new + modify + cancel), dominated by algorithmic traders in derivatives: design for ~1 million messages/s overall at peak.
- Instruments: a few thousand equities plus a very large number of option contracts. Activity is extremely skewed: index options near the money get a big share of the messages.
- Journal: 1M msgs/s × ~100 bytes = 100 MB/s, ~2 TB over a 6-hour session. Sequential disk writes handle this.
- Latency target inside matching: single-digit to tens of microseconds per message.
Broker (assumed)
- 10M clients, 2M active on a busy day, with a large share logging in between 9:00 and 9:20.
- Orders: ~10M/day; at the open maybe 20K–50K orders/s for the first minute.
- Price streaming: 1M concurrent WebSocket connections at peak, each subscribed to ~30 instruments. If each instrument update is conflated to once per second per client, that's ~30M messages/s outbound at ~50 bytes ≈ 1.5 GB/s (~12 Gbps). This is the broker's biggest scaling problem, not order placement.
The Order Book Data Structure
For one instrument, the book has two sides:
Operations and their target complexity:
| Operation | Structure | Complexity |
|---|---|---|
| Best bid / best ask | Sorted map's first element (or cached pointer) | O(1) |
| Add order at a price | Find/create level, append to FIFO | O(log L) with a tree, O(1) with a price-indexed array (L = number of levels) |
| Cancel by order ID | Hash map ID → (level, list node), unlink | O(1) |
| Match against best level | Pop from FIFO front | O(1) per fill |
| Depth snapshot (top 5 levels) | Iterate from best | O(5) |
Why not a heap? A priority queue gives you the best price, but cancelling an arbitrary order (most exchange messages are cancels and modifies) is O(n) to find, and you lose the per-level aggregation needed for depth data. The sorted map of levels + FIFO queues + ID hash map is the standard answer. (If you've practised heap problems, this is a good contrast to articulate.)
Production engines often use an array indexed by price tick: prices move in fixed ticks (₹0.05 for many Indian equities) within a daily price band, so a band of a few thousand ticks maps directly to array slots, giving O(1) level access and excellent cache locality.
The Matching Engine
Rules: an incoming buy matches resting asks while the best ask ≤ its limit price (any price for market orders); fills happen at the resting order's price; within a level, earliest first. Any unfilled remainder of a limit order rests in the book; a market or IOC remainder is cancelled.
#include <cstdint>
#include <functional>
#include <iostream>
#include <list>
#include <map>
#include <unordered_map>
#include <vector>
enum class Side { BUY, SELL };
struct Order {
uint64_t id;
Side side;
int64_t price; // in paise; ignored for market orders
int64_t qty; // remaining quantity
};
struct Trade { uint64_t buyId, sellId; int64_t price, qty; };
class OrderBook {
// Bids: highest price first. Asks: lowest price first. FIFO within a level.
std::map<int64_t, std::list<Order>, std::greater<int64_t>> bids;
std::map<int64_t, std::list<Order>> asks;
struct Loc { Side side; int64_t price; std::list<Order>::iterator it; };
std::unordered_map<uint64_t, Loc> index; // order id -> position, for O(1) cancel
template <typename Book>
void match(Order& in, Book& book, bool isMarket, std::vector<Trade>& out) {
while (in.qty > 0 && !book.empty()) {
auto level = book.begin(); // best opposite price
bool crosses = isMarket ||
(in.side == Side::BUY ? level->first <= in.price : level->first >= in.price);
if (!crosses) break;
auto& queue = level->second;
while (in.qty > 0 && !queue.empty()) {
Order& resting = queue.front(); // time priority
int64_t q = std::min(in.qty, resting.qty);
out.push_back(in.side == Side::BUY
? Trade{in.id, resting.id, level->first, q}
: Trade{resting.id, in.id, level->first, q}); // at resting price
in.qty -= q;
resting.qty -= q;
if (resting.qty == 0) { index.erase(resting.id); queue.pop_front(); }
}
if (queue.empty()) book.erase(level);
}
}
template <typename Book>
void addResting(const Order& o, Book& book) {
auto& queue = book[o.price];
queue.push_back(o);
index[o.id] = Loc{o.side, o.price, std::prev(queue.end())};
}
public:
std::vector<Trade> submitLimit(Order o) {
std::vector<Trade> trades;
if (o.side == Side::BUY) { match(o, asks, false, trades); if (o.qty > 0) addResting(o, bids); }
else { match(o, bids, false, trades); if (o.qty > 0) addResting(o, asks); }
return trades;
}
std::vector<Trade> submitMarket(Order o) { // unfilled remainder is cancelled
std::vector<Trade> trades;
if (o.side == Side::BUY) match(o, asks, true, trades);
else match(o, bids, true, trades);
return trades;
}
bool cancel(uint64_t id) {
auto f = index.find(id);
if (f == index.end()) return false;
Loc loc = f->second;
if (loc.side == Side::BUY) {
auto lvl = bids.find(loc.price);
lvl->second.erase(loc.it);
if (lvl->second.empty()) bids.erase(lvl);
} else {
auto lvl = asks.find(loc.price);
lvl->second.erase(loc.it);
if (lvl->second.empty()) asks.erase(lvl);
}
index.erase(f);
return true;
}
void print() const {
for (auto it = asks.rbegin(); it != asks.rend(); ++it) {
int64_t total = 0; for (auto& o : it->second) total += o.qty;
std::cout << " ASK " << it->first / 100.0 << " x " << total << "\n";
}
for (auto& lvl : bids) {
int64_t total = 0; for (auto& o : lvl.second) total += o.qty;
std::cout << " BID " << lvl.first / 100.0 << " x " << total << "\n";
}
}
};
int main() {
OrderBook book;
book.submitLimit({1, Side::SELL, 250050, 100}); // sell 100 @ 2500.50
book.submitLimit({2, Side::SELL, 250100, 200}); // sell 200 @ 2501.00
book.submitLimit({3, Side::SELL, 250050, 50}); // sell 50 @ 2500.50 (behind #1)
book.submitLimit({4, Side::BUY, 249900, 300}); // buy 300 @ 2499.00
book.print();
auto trades = book.submitLimit({5, Side::BUY, 250100, 250}); // aggressive buy
for (auto& t : trades)
std::cout << "TRADE buy#" << t.buyId << " sell#" << t.sellId
<< " " << t.qty << " @ " << t.price / 100.0 << "\n";
book.cancel(4);
book.print();
}
ASK 2501 x 200
ASK 2500.5 x 150
BID 2499 x 300
TRADE buy#5 sell#1 100 @ 2500.5
TRADE buy#5 sell#3 50 @ 2500.5
TRADE buy#5 sell#2 100 @ 2501
ASK 2501 x 100
Dry run. Order #5 wants 250 at up to ₹2,501.00. The best ask is ₹2,500.50 with #1 (100) then #3 (50) in time order: both fill completely at ₹2,500.50, 150 done. The level empties and is removed. Next best ask ₹2,501.00 ≤ 2,501.00, so #2 fills 100 of its 200, leaving 100 resting. #5 is fully filled and never rests. Cancelling #4 removes the only bid in O(1) via the index. Note prices are integers in paise; floating-point prices in an order book are a correctness bug waiting to happen.
Beyond the basics
- Modify: reducing quantity usually keeps time priority; changing price or increasing quantity is a cancel + new order (loses priority).
- Stop-loss orders sit in a separate trigger book keyed by trigger price; when the last traded price crosses the trigger, they're injected into the main book as limit/market orders, in sequence.
- Self-trade prevention: reject or cancel when a member's buy would match its own sell.
- Pre-open call auction: collect orders without matching, compute the single equilibrium price that maximises traded volume, match everything at that price, then switch to continuous matching.
The Sequencer and Deterministic Replay
Fairness at an exchange means a single, total order of events: if order A arrived before order B, A gets priority, and everyone can verify it. That's the job of the sequencer.
- Gateways receive member messages, validate them and forward to the sequencer.
- Sequencer assigns a monotonically increasing sequence number and timestamp to each message and writes it to a journal (durable, replicated) before it is processed.
- Matching engine consumes the sequenced stream in order, single-threaded. Its output (acks, trades, book changes) is itself sequenced and published.
- Replicas consume the same sequenced input and run identical engine code. Because the engine is deterministic (no wall-clock reads, no randomness, no thread scheduling effects; time comes from the sequenced message), every replica holds an identical book.
- Failover: if the primary engine dies, a hot standby that has processed up to sequence N takes over. Nothing is lost because the input was journaled before processing.
- Recovery from scratch: load the latest snapshot of the book, replay the journal from the snapshot's sequence number.
Partitioning
One thread can't handle every instrument at peak. Partition instruments across engine instances (e.g. by symbol), each with its own sequenced stream. Instruments within a partition are processed strictly in order; across partitions there's no ordering requirement, because orders for different instruments never interact. Hot instruments (index options near expiry) get their own partitions. Cross-instrument features (spread orders across two contracts) are the exception and need careful design.
Low-Latency Techniques
| Technique | Why it helps |
|---|---|
| Single-threaded hot path per partition | No locks, no contention, no context switches; caches stay warm |
| Ring buffers between stages (Disruptor-style) | Pre-allocated, lock-free handoff; mechanical sympathy with CPU caches |
| Pre-allocated object pools, no allocation on hot path | Avoids GC pauses (Java) or allocator latency (C++) |
| CPU pinning, isolated cores, busy-spin waiting | Removes scheduler jitter; trades CPU for latency |
| Kernel-bypass networking (e.g. DPDK, specialised NICs) | Avoids syscalls and kernel network stack overhead |
| Binary protocols, fixed-size messages | No JSON parsing; predictable sizes |
| Journaling with sequential writes, batched fsync or replication to memory of standbys | Durability without per-message disk seeks |
| Colocation | Exchanges offer racks near the matching engines; distance is latency |
| Measure tail latency (p99, p99.9) | Averages hide the jitter that matters |
Note what's missing: microservices, service meshes, Kafka, and databases on the matching path. Each adds milliseconds; the engine budget is microseconds. A strong interview answer explicitly says which "standard" tools you are not using and why.
Market Data Fan-Out
At the exchange
The engine's output is turned into market data: trades, top-of-book quotes, market depth and periodic snapshots. Exchanges typically distribute it via UDP multicast to members (one send reaches all subscribers on the network), with sequence numbers so receivers detect gaps, plus a TCP recovery/retransmission service and periodic snapshots so a late joiner can rebuild state.
At the broker: millions of phones
- Feed handlers consume the exchange feed, normalise it and maintain the latest state per instrument (LTP, OHLC, volume, depth) in memory.
- Distribution tier: the latest state is published to a fleet of WebSocket servers. Each server keeps, per instrument, the set of connected clients subscribed to it.
- Conflation: a retail client doesn't need every tick. Each server sends the latest snapshot per subscribed instrument at a fixed interval (e.g. every second), coalescing intermediate ticks. This caps bandwidth regardless of market activity.
- Modes: LTP-only (tiny), quote, and full depth, chosen per subscription, so a watchlist of 50 doesn't pull 50 depth books.
- Binary encoding rather than JSON for ticks; the savings matter at millions of messages per second.
WebSockets fit because the stream is long-lived, bidirectional (subscribe/unsubscribe messages) and high-frequency; compare the transports in WebSockets vs SSE vs long polling. At 9:15 the connection storm is as dangerous as the message volume: pre-scale WebSocket servers, spread reconnects with jittered backoff, and keep the subscribe path cheap.
Broker Design: OMS, RMS and the Exchange Gateway
- OMS (Order Management System): owns the order state machine (
VALIDATION_PENDING → OPEN_PENDING → OPEN → PARTIALLY_FILLED → COMPLETE / CANCELLED / REJECTED). Every exchange response is applied idempotently by exchange order ID and update sequence. - RMS (Risk Management System): pre-trade checks must be fast and correct: available margin (including SEBI-mandated margin rules for derivatives), short-sell restrictions, quantity freeze limits, price bands, per-client and per-instrument exposure limits. Margin is blocked atomically when the order is placed and released on cancel/reject. Keep each client's margin state in memory on a partitioned RMS service (partitioned by client ID) backed by a durable ledger, so checks are microseconds-to-milliseconds, not DB round trips.
- Exchange gateway: maintains the member's connections to the exchange, enforces the exchange's per-connection message rate limits by queuing and throttling, and handles reconnection and re-synchronisation (asking the exchange for order statuses after a disconnect).
- GTT orders and alerts: stored on the broker side and evaluated against the live price stream; when a trigger hits, the broker places a regular order. This is a mini matching problem: index triggers by instrument and price, and scan only the price range crossed since the last tick.
- Funds: pay-in via UPI/netbanking and pay-out to bank; a double-entry ledger per client as in a digital wallet.
Surviving 9:15 AM
- Pre-scale and pre-warm everything before 9:00; logins and session creation spike first.
- Accept AMO (after-market orders) the previous evening and queue them for submission at the open, smoothing the burst through the gateway's rate limits.
- Isolate order placement from portfolio/report/charts traffic so a heavy positions page can't block orders.
- Degrade non-critical features (charts history, analytics) first; never degrade order status accuracy.
Post-Trade: Clearing and Settlement
After matching, the clearing corporation steps in as the counterparty to both sides (novation), so neither trader carries the other's default risk. It nets each member's obligations, collects margins, and on settlement day shares move between demat accounts at the depositories (NSDL/CDSL) while funds move between banks. For Indian equities settlement is T+1, with an optional same-day settlement cycle introduced for a limited set of stocks. Brokers reconcile their internal trade book with exchange and clearing reports every day, the same reconciliation discipline described in payment system design.
Interview Angle
What interviewers are probing
- Order book fluency: can you write the data structure and explain cancel in O(1)?
- Why single-threaded? Determinism, no locks, replayability; scaling by partitioning instruments.
- Durability without latency: journal before processing, replicate the input log, hot standby replay.
- Fairness: sequencing defines who was first; the gateway must not reorder messages unfairly across members.
- Broker realism: RMS before routing, idempotent application of exchange updates, the market-open spike, conflated market data.
Common mistakes
- Putting a relational database or Kafka on the matching hot path.
- Multi-threaded matching on the same book with locks.
- Using a heap for the book and ignoring cancels.
- Floating-point prices.
- Pushing every tick to every client instead of conflating per subscription.
- Mixing up broker and exchange responsibilities ("Zerodha matches my order with another Zerodha user").
Good follow-up answers to rehearse
- "Primary engine crashes mid-burst." Standby has replayed the journal to sequence N; it takes over from N+1; members reconnect and gateways resend any unacknowledged messages, deduplicated by client order ID.
- "How do you test a matching engine?" Deterministic replay of recorded sessions, invariant checks (book never crossed after matching, quantities conserved), and property-based tests on random order streams.
- "How do you show a user's P&L live?" Positions × latest LTP from the in-memory price cache, computed client-side or on the push tier; no database round trip per tick.
The trading system is one of the few design problems where the right answer is to avoid distribution on the critical path. If you can explain that clearly, and still design a horizontally scaled broker around it, you'll stand out. For a contrast in ranking under heavy update load, see the real-time leaderboard, which faces a similar "one event changes many derived views" problem.
Frequently Asked Questions
Prepare for Fintech System Design Interviews
Prepflix's system design course covers trading, payments and other low-latency fintech systems with mock interviews.
Explore the Prepflix Course →