Design a Real-Time Stock Trading System
A stock trading system must accept orders, match buy and sell orders in real time, and produce correct executions under strict latency and consistency constraints. In interviews, the matching engine is the core: it must enforce price-time priority (best price first, then earliest time).
This walkthrough follows the Interview Framework and focuses on what you'd present in a 45-60 minute interview.
Phase 1: Requirements (~5 minutes)
Functional Requirements
- Users should be able to place buy/sell limit orders with
(symbol, side, price, quantity, time) - Users should be able to cancel open orders
- System should match orders in real time using price-time priority
- Users should be able to query order status and executed trades
- Clients should receive live market updates (top of book and trade ticks)
Matching rule: for each symbol, match highest buy price with lowest sell price; for equal price, earliest order time wins.
Non-Functional Requirements
| Requirement | Target |
|---|---|
| Latency | p99 match decision < 10ms per order |
| Throughput | 100K orders/sec peak across symbols |
| Availability | 99.99% for order entry and market data |
| Consistency | Strong ordering within each symbol partition |
| Durability | No lost accepted orders or executed trades |
Capacity Estimation
Assume:
- 10M daily active traders
- 1B orders/day
- Peak factor 10x around market open
Average QPS = 1,000,000,000 / 86,400 ~= 11,600 orders/sec
Peak QPS ~= 116,000 orders/sec
Order size ~= 200 bytes
Ingress/day ~= 200 GB raw (before replication/indexes)
In interviews, emphasize that this scale is manageable if you partition by symbol and keep each partition single-writer.
Phase 2: Data Model (~5 minutes)
Core Entities
Order
├── order_id (PK)
├── user_id
├── symbol
├── side: BUY | SELL
├── type: LIMIT | MARKET
├── price (nullable for MARKET)
├── quantity
├── remaining_quantity
├── status: NEW | PARTIALLY_FILLED | FILLED | CANCELED | REJECTED
├── created_at (exchange timestamp)
└── partition_key (symbol)
Trade
├── trade_id (PK)
├── symbol
├── buy_order_id
├── sell_order_id
├── price
├── quantity
├── executed_at
└── sequence_number (monotonic per symbol)
OrderBookLevel
├── symbol
├── side
├── price
├── aggregate_quantity
└── order_count
Account
├── user_id (PK)
├── cash_balance
├── buying_power
└── updated_at
Relationships
Ordergenerates zero or moreTraderows until fully filled/canceled.Tradeis immutable and drives downstream positions, balances, and market data.OrderBookLevelis derived state for fast read APIs/WebSocket streams.
Phase 3: API Design (~5 minutes)
Protocol Choices
- Client order entry: REST over HTTPS (simple and standard)
- Low-latency internal services: gRPC
- Live market stream / order updates: WebSocket
If interviewer asks for HFT-grade design, mention binary TCP/UDP protocols (e.g., FIX/OUCH style). For general interviews, REST + WebSocket is acceptable.
Key Endpoints
POST /v1/orders
{
"symbol": "AAPL",
"side": "BUY",
"type": "LIMIT",
"price": 180.25,
"quantity": 100,
"client_order_id": "abc-1"
}
Response: { "order_id": "o789", "status": "NEW" }
user_id should come from the authenticated session/JWT, not from client payload, to prevent order spoofing.
POST /v1/orders/{order_id}/cancel
Response: { "order_id": "o789", "status": "CANCELED" }
GET /v1/orders/{order_id}
Response: { "status": "PARTIALLY_FILLED", "filled_quantity": 40, ... }
GET /v1/order-book/{symbol}?depth=20
Response: { "bids": [[180.25, 500]], "asks": [[180.30, 300]] }
WS /v1/stream
Channels: order_updates, trades, top_of_book, depth
Phase 4: High-Level Design (~15-25 minutes)
Core Components
| Component | Responsibility |
|---|---|
| API Gateway | Auth, idempotency key checks, request normalization |
| Risk Service | Balance/position limits before accepting an order |
| Symbol Router | Routes each order to exactly one matching partition |
| Matching Engine | Maintains in-memory book and executes price-time matching |
| Durable Event Log | Append-only source of truth for recovery/replay |
| Market Data Service | Builds snapshots/deltas and pushes live updates |
| Clearing Service | Settles trades and updates balances/positions |
Matching Flow
- Client sends order to API; system authenticates and checks risk.
- Router maps
symbol -> partition(single writer per symbol). - Partition leader appends the order command to replicated durable log.
- Matching engine applies the command in sequence, checks opposite-side best price, and generates trade events.
- Engine appends resulting events (
order accepted,trade executed,order filled) and ACKs only after replication quorum. - Async consumers update DB projections and push WebSocket updates.
Price-Time Priority Implementation
- Buy book: max-heap by price, FIFO queue at each price level
- Sell book: min-heap by price, FIFO queue at each price level
- For equal price, dequeue oldest order first
To preserve fairness, use exchange-assigned sequence/timestamp from the matching engine, not client-provided time.
Phase 5: Scaling & Trade-offs (~15-20 minutes)
How This Meets Non-Functional Goals
- Low latency: in-memory order books and single-threaded matching loop per partition
- High throughput: horizontal scaling by symbol partitions
- Consistency: total order guaranteed inside each symbol partition
- Durability: write-ahead append to replicated log before acking order acceptance
- Availability: active-standby engine replicas with fast leader failover
Bottlenecks and Solutions
| Bottleneck | Why it Happens | Mitigation |
|---|---|---|
| Hot symbols (e.g., AAPL) | Skewed traffic to one partition | Pin hot symbols to dedicated partitions/machines; keep one writer per symbol |
| WebSocket fanout pressure | Many subscribers per symbol | Hierarchical fanout and regional edge pub/sub |
| Recovery time after crash | Replaying long event history | Frequent snapshots + incremental log replay |
| Risk service latency | Synchronous check on order path | Local caches + bounded stale reads with hard limits |
Key Trade-offs
- Strong ordering vs availability
- Strong ordering per symbol requires one active leader; failover can briefly pause writes.
- Latency vs durability
- Waiting for replicated log ack increases latency but avoids data loss.
- Global fairness vs scalability
- We guarantee fairness per symbol partition, not global cross-symbol ordering.
Deep Dive: Cancel vs Fill Race
When cancel and fill arrive close together, resolve by partition sequence order:
- If cancel is sequenced first, remaining quantity is canceled.
- If fill is sequenced first, execution stands; cancel applies only to leftover quantity.
This deterministic rule prevents inconsistent states across services.
Common Pitfalls
Using client timestamps for order priority causes unfairness and clock-skew exploits. Always use server sequencing.
Multiple writers on one symbol can violate price-time ordering. Keep one logical writer per symbol partition.
Acknowledging before durability risks lost accepted orders during crashes. Persist first, then ACK.
Treating market data and matching as one service can cause backpressure from slow consumers to affect core matching latency.
Interview Checklist
- Clarified order types and matching rule (price-time priority)
- Stated latency/throughput/consistency targets
- Proposed symbol partitioning with single-writer matching
- Explained durability strategy (event log + replay)
- Covered cancel/fill race and hot-symbol scaling
Summary
| Area | Design Choice |
|---|---|
| Core architecture | Partitioned matching engines with in-memory books |
| Ordering guarantee | Strong per-symbol ordering via single writer |
| Source of truth | Append-only durable event log |
| External APIs | REST for orders, WebSocket for live updates |
| Primary risks | Hot partitions, failover gaps, fanout load |