0) Problem Restatement
Design a brokerage app where users see live stock prices, place buy and sell orders, and track their orders, holdings and balance. The broker does not match trades itself. It sends orders to a stock exchange (like NSE or NASDAQ), which matches buyers and sellers and reports back fills (executions).
Key challenges:
- Never let a user spend money they don't have.
- Keep every order's status correct, even when messages from the exchange are delayed or repeated.
- Survive the huge spike when the market opens.
1) Requirements
1.1 Functional
- Live quotes for stocks.
- Place orders: market (buy now at the best price), limit (buy only at ₹100 or lower), stop-loss. Modify and cancel orders.
- Show order status: open, partially filled, filled, cancelled, rejected.
- Show holdings, P&L and available funds.
1.2 Non-Functional
- Correctness of money and positions above everything.
- Low latency from order click to exchange (tens of milliseconds inside our system).
- High availability during market hours.
- Full audit trail for regulators.
1.3 Scale Estimates
- 10M active users. At market open, 50K orders/sec for the first minutes.
- Quotes: 5,000 stocks with many ticks per second each, fanned out to about 2M connected users.
- 20M orders/day. Each order has ~5 events, so about 100M order events/day.
1.4 API Design
POST /v1/orderswith headerIdempotency-Keyand body{ symbol, side: buy, type: limit, qty: 10, price: 2450.50 }→{ order_id, status: "open" }DELETE /v1/orders/{id}(cancel),PATCH /v1/orders/{id}(modify)GET /v1/orders?status=open,GET /v1/portfolio- WebSocket
/v1/quotes→ subscribe to symbols
2) High-Level Architecture
2.1 Overview
- Order Service / OMS (Order Management System): validates orders and tracks their state.
- Risk & Funds Service: checks margin and places a hold on funds (for buys) or shares (for sells).
- Exchange Gateway: keeps persistent connections to the exchange using the FIX protocol (the standard trading message format), sends orders and receives fills.
- Ledger / Positions: the record of cash and shares per user.
- Market Data Service: receives the exchange price feed and streams quotes to users.
- Kafka: the order events log, used by notifications, portfolio updates and audit.
2.2 Architecture Diagram
Architecture Diagram
flowchart LR
U["Mobile / Web"] --> API["API Gateway"]
API --> OMS["Order Service - state machine"]
OMS --> RISK["Risk and Funds - holds"]
RISK --> LED[("Ledger and Positions DB")]
OMS --> DB[("Orders DB")]
OMS --> EG["Exchange Gateway - FIX"]
EG <--> EX["Stock Exchange"]
EG -->|"fills, rejects"| OMS
OMS --> K[("Order events - Kafka")]
K --> NOTIF["Notifications"]
K --> AUD["Audit store"]
EX -->|"price feed"| MD["Market Data Service"]
MD -->|"WebSocket quotes"| U3) Data Model
CREATE TABLE orders (
order_id UUID PRIMARY KEY,
user_id UUID,
symbol TEXT,
side TEXT, -- buy / sell
type TEXT, -- market / limit / stop
qty INT,
filled_qty INT DEFAULT 0,
limit_price NUMERIC(12,2),
status TEXT, -- pending, open, partially_filled, filled, cancelled, rejected
exchange_order_id TEXT,
version INT
);
CREATE TABLE ledger_entries ( -- append-only money and share movements
entry_id UUID, user_id UUID, asset TEXT, -- 'INR' or a symbol
amount NUMERIC, kind TEXT, -- hold, release, trade, fee
order_id UUID, created_at TIMESTAMP
);
CREATE TABLE fills (exec_id TEXT PRIMARY KEY, order_id UUID, qty INT, price NUMERIC, ts TIMESTAMP);
fills.exec_id is the exchange's unique execution ID. Using it as the key means a repeated fill message is ignored.
4) Key Flows
4.1 Placing a buy order
- The API checks the idempotency key, so a double tap does not create two orders.
- The Risk service checks available funds and places a hold of
qty × limit price(plus fees). The money is now reserved. - The OMS saves the order as
pendingand sends it to the exchange through the gateway. - The exchange acknowledges →
open. - Fills arrive (maybe several partial fills). For each new
exec_id: updatefilled_qty, turn the matching part of the hold into a real debit, add shares to positions, and publish an event. - When fully filled →
filled, and release any unused hold.
4.2 Cancel
Send a cancel to the exchange. The order is only cancelled when the exchange confirms, because a fill might arrive first. The rest of the hold is then released.
5) Deep Dive A — Correctness of money and state
- State machine: only allowed transitions (e.g.,
filledcan never go back toopen). Use theversioncolumn (optimistic locking) so two updates don't overwrite each other. - Idempotency everywhere: client idempotency key on create,
exec_idfor fills, and our own order ID sent to the exchange as the client order ID. - Append-only ledger: balances are the sum of entries, and holds are entries too. Nothing is edited in place, which gives regulators a full history.
- End-of-day reconciliation: compare our fills and positions with the exchange's trade file and the clearing house. Mismatches are investigated before the next day.
6) Deep Dive B — Market open and quotes
- Spike at 9:15 AM: pre-scale before open, accept orders into a queue per exchange connection, and apply per-user rate limits. Orders placed before open ("AMO", after-market orders) are sent in a controlled stream.
- Quotes to millions: the market data service receives the exchange feed once, then fans it out through many WebSocket servers. Users subscribe to specific symbols, and we send throttled updates (e.g., at most 4 per second per symbol per user), because the human eye can't use more.
- In-memory OMS (LLD variant): keep open orders in memory, indexed by order ID and by symbol. Write every change to a log first so the OMS can recover after a crash by replaying it.
7) Trade-offs & Alternatives
| Decision | Choice | Why | Alternative |
|---|---|---|---|
| Funds check | Hold before sending the order | Can't overspend | Check after fill: risky |
| Order store | SQL with strict state machine | Correctness, audit | NoSQL: scales easily, weaker transactions |
| Fill handling | Dedupe by exec_id | Safe against repeats | Trust the exchange never repeats: unsafe |
| Quotes | Throttled WebSocket fan-out | Scales to millions | Push every tick: wastes bandwidth |
8) Common Follow-up Questions
- "What if the gateway loses connection mid-order?" The FIX protocol has sequence numbers. On reconnect, both sides replay missed messages. Until confirmed, the order stays
pending. - "How are stop-loss orders handled?" Either the exchange supports them natively, or our service watches prices and places a market order when the trigger price is hit.
- "Portfolio P&L?" Positions × latest price. Compute it on the client from streamed prices to avoid heavy server work.
9) Wrap-Up
Validate, hold funds, then send orders to the exchange through a FIX gateway. Track each order with a strict state machine, apply fills idempotently by execution ID to an append-only ledger, and reconcile with the exchange every day. Handle market-open spikes with pre-scaling and queues, and stream throttled quotes over WebSockets.