Interview Setup
Interview Prompt
Design a cryptocurrency exchange handling 100K orders/sec peak across 500 trading pairs, with in-memory matching, hot/cold wallet architecture, and 2M concurrent WebSocket price feeds.
Clarifying Questions (ask before designing)
| Question | Why it matters |
|---|---|
| Custodial (exchange holds keys) or non-custodial (user holds keys)? |
|
| Matching engine: price-time priority like a stock exchange? | 100K orders/sec needs in-memory order book per pair with WAL. |
| How many blockchain confirmations before crediting deposit? |
|
| Withdrawal limits and multi-sig for large amounts? |
|
Scope
In scope
- Variant of stock exchange + wallet
- Hot/cold wallet
- Blockchain confirmation
- Order matching
- Capacity estimation with shown math
Out of scope (state explicitly)
- Fraud ML model training: rules engine is sufficient unless specifically asked
- Merchant onboarding / KYC workflows
- Building a PSP or bank from scratch
Functional Requirements
Start by asking your interviewer whether you are designing the matching engine, custody layer, or both. Clarify regulatory scope (KYC/AML, fiat on-ramps) and confirm idempotency plus audit trail requirements before you sketch boxes because money correctness is non-negotiable.
- Order placement: limit, market, stop-loss orders for crypto pairs
- Order matching engine: price-time priority
- Wallet management: deposit, withdraw crypto (on-chain) and fiat
- Hot wallet (online, for fast withdrawals) and cold wallet (offline, for security)
- Real-time order book and trade feed (WebSocket)
- Portfolio: view balances, P&L, transaction history
- KYC/AML: identity verification, transaction monitoring
- Multi-factor authentication (2FA, hardware keys)
- Trading fees with tiered pricing (maker/taker)
- Staking and lending features
Non-Functional Requirements
Your interviewer will test whether you understand the latency split: sub-millisecond matching vs async settlement. Lead with "balances must never be wrong" and hot/cold wallet blast-radius before discussing throughput.
- Low Latency: Order matching < 1ms; API response < 50ms
- High Availability: 99.99% (downtime during volatility = massive losses)
- Security: Cold wallet for 95% of assets; HSM for key management
- Consistency: Account balances must NEVER be wrong
- Auditability: Every transaction traceable for regulatory compliance
- Throughput: 100K orders/sec peak (during market events)
Capacity Estimations
Size the matching engine per trading pair because single-threaded in-memory books avoid lock contention at 100K orders/sec peak. Wallet and WebSocket fan-out numbers justify why settlement stays async.
| Metric | Calculation | Value |
|---|---|---|
| Registered users | Given | 50M |
| DAU | Given | 5M |
| Trading pairs | Given | 500 |
| Orders / sec (peak) | Derived from daily volume ÷ 86400 (+ peak factor) | 100K |
| Trades / sec (peak) | Derived from daily volume ÷ 86400 (+ peak factor) | 50K |
| WebSocket connections | Given | 2M concurrent |
| Wallet transactions / day | 1M ÷ 86400 | 1M |
| Hot wallet balance | Given | 5% of total assets |
Architecture Diagram
In the room: draw matching engine and wallet layer as separate boxes: the hot wallet holds approximately 5% of assets, while cold storage limits breach blast radius.
In the interview, draw the matching engine and wallet layer as separate boxes with Kafka between them. A crypto exchange is two systems welded together: a sub-millisecond matching engine where price-time priority and WAL durability are non-negotiable, and a custodial wallet layer where hot and cold tiers limit blast radius on breach. Kafka bridges the match event to async settlement so balance updates never block the order ACK path.
At 100K orders/sec peak across 500 pairs, each pair runs single-threaded in memory without lock contention, yielding predictable microsecond latency. WebSocket feeds fan out trade and order-book updates to 2M concurrent connections; edge throttling prevents a volatility spike from melting the broadcast tier.
Component Deep Dives
Money paths split into a synchronous commit phase (reserve funds, match, WAL append) and an async settlement phase (balance deduction, fees, notifications). The sections below trace that lifecycle through the matching engine, wallet tiers, race-condition guards, and the Kafka topics that make recovery after a crash replayable.
Order Lifecycle
The Order Service validates and reserves funds in Redis before the matching engine ever sees the order, meaning a failed reserve returns immediately without polluting the book. Settlement consumers apply PostgreSQL ledger updates idempotently from trade events.
1. User submits order: POST /api/orders {pair:BTC_USDT, side:buy, price:50000, qty:0.5}
2. Order Service:
a. Validate: user authenticated, pair exists, qty > min
b. Reserve funds: Lua script in Redis → USDT_available -= 25000
c. If Redis reserve fails → reject immediately
d. Persist order (status=open) to PostgreSQL
e. Route to matching engine for this pair
3. Matching Engine (single-threaded, in-memory per pair):
a. Insert order into order book (TreeMap of price levels)
b. Match against opposite side: buy $50K vs best ask $49,950 → MATCH at $49,950
c. Generate trade event. Write to WAL before returning.
d. Publish trade event to Kafka
4. Post-Trade Settlement (async from Kafka):
a. Update balances, deduct fees (maker 0.1%, taker 0.15%)
b. All inside PostgreSQL transaction
5. WebSocket broadcast: order book update, trade feed, user eventsMatching Engine Deep Dive
Price-time priority means the best bid meets the best ask at the maker's price, and FIFO queueing within a price level prevents queue-jumping. Single-threaded execution per pair eliminates lock contention and makes cancel-during-match semantics deterministic.
Data structure: Two TreeMaps (Red-Black Trees) per pair Bids: sorted by price DESC (highest first), then time ASC Asks: sorted by price ASC (lowest first), then time ASC Price-Time Priority: same price → earlier order fills first (FIFO) Single-threaded: ONE thread per trading pair No locks needed → predictable µs latency 100 pairs yields 100 independent threads WAL: Every match written to append-only file BEFORE publish On crash → replay WAL → rebuild order book state Performance: 100K+ matches/sec per pair (in-memory, single-threaded)
Deposit & Withdrawal Flow
Deposits wait for chain confirmations before crediting, so a reorg deeper than the threshold reverses pending credits. Withdrawals drain the hot wallet first, while large amounts queue for cold-wallet multi-sig with human review on anomaly signals.
DEPOSIT: 1. Generate HD wallet address (unique per user per chain) 2. Blockchain Watcher detects incoming transaction 3. Wait for N confirmations (BTC:3, ETH:12, SOL:32) 4. Credit user: UPDATE balances SET available = available + amount 5. Edge case: chain reorg → reverse credit if tx disappears WITHDRAWAL: 1. User submits withdrawal (2FA required) 2. Deduct from available balance (PG transaction) 3. Hot wallet service signs transaction (HSM multi-sig 2-of-3) 4. Broadcast to blockchain 5. Monitor for confirmation 6. If hot wallet balance < withdrawal amount: queue and trigger cold-to-hot replenishment
Race Conditions
Double-spend, cancel-during-match, and withdraw-while-trading are the three classic failure modes. Redis Lua and PostgreSQL SELECT FOR UPDATE split available vs reserved balances so concurrent orders cannot oversell the same coins.
1. Double-Spend: User has 1 BTC, submits two sell orders simultaneously Solution: Redis Lua atomic check-and-decrement (single-threaded, no race) PostgreSQL: authoritative backup with SELECT FOR UPDATE 2. Order Cancel During Matching: Solution: Single-threaded engine → cancel queued behind match If order partially filled before cancel: fill matched, cancel remainder 3. Withdrawal After Balance Used in Trade: Solution: Withdrawal deducts from available, trade from reserved (different balance pool) PostgreSQL transaction ensures atomicity
Blockchain Reorg Handling
The system waits for N confirmations before crediting deposits (such as 3 confirmations or approximately 30 minutes on Bitcoin, and 12 confirmations or approximately 3 minutes on Ethereum). The Blockchain Watcher compares block hashes continuously. If a reorg is detected deeper than N blocks, the system reverses pending credits. The exchange never permits users to trade against unconfirmed deposits.
Event Bus Design (Kafka)
Trade events partition by trading pair to preserve per-book ordering, allowing settlement, WebSocket broadcast, and audit consumers to replay from the same durable log. WAL fsync precedes Kafka publish so a crash never loses a matched trade that was ACKed to the client.
Topic: trade-events
Partitions: 128 (partition by trading_pair to preserve per-pair ordering)
Retention: compacted by trade_id (compliance; replay rebuilds order book state)
Producers: Matching Engine immediately after WAL append (before ACK to client)
Payload: {trade_id, pair, price, qty, buyer_id, seller_id, maker_order_id, taker_order_id}
Consumers: Settlement Service, WebSocket broadcaster, audit log, market data feed
Topic: order-events
Partitions: 64 (partition by order_id)
Events: placed, partially_filled, cancelled, expired
Consumers: Order Service state sync, user notification WebSocket
Topic: wallet-events
Events: deposit_confirmed, withdrawal_submitted, withdrawal_broadcast, balance_adjusted
Consumers: Blockchain Watcher, AML monitoring, Proof-of-Reserves aggregator
Match path: in-memory match → WAL fsync → publish trade-events → return fill to client
Settlement (balance update, fees) consumes trade-events asynchronouslyAPI Design
REST APIs handle order placement and account balances, while WebSocket streams power real-time order-book and trade execution feeds. Every order submission must provide an idempotency key on POST /orders because duplicate submissions during market volatility spikes are a classic failure mode.
POST /api/orders → Place order {pair, side, type, quantity, price}
DELETE /api/orders/{id} → Cancel order
GET /api/orders?status=open → Open orders
GET /api/orderbook/{pair} → Order book snapshot
GET /api/trades/{pair} → Recent trades
GET /api/wallet/balances → Portfolio balances
POST /api/wallet/withdraw → Initiate withdrawal (2FA required)
# WebSocket streams
WS /ws/orderbook/{pair} → Real-time order book
WS /ws/trades/{pair} → Real-time trades
WS /ws/user → User order updates, balance changesCommon Error Responses
400 Bad Request: invalid input, missing required fields, or malformed JSON payload 401 Unauthorized: missing or invalid authentication token or API key 403 Forbidden: authenticated caller lacks required permissions for this resource 404 Not Found: requested resource ID does not exist 409 Conflict: duplicate write or version conflict, retry with a unique idempotency key 422 Unprocessable Entity: syntactically valid request failed semantic business validation 429 Too Many Requests: rate limit quota exceeded, client should honor Retry-After header 500 Internal Error: unexpected server failure, retry safely with an idempotency key 503 Service Unavailable: downstream dependency is unavailable or overloaded, retry with exponential backoff
Data Model
PostgreSQL (Ledger: Source of Truth)
ACID transactions protect all financial ledger operations. DECIMAL(28,18) handles cryptocurrency precision (supporting 18 decimal places for Ethereum), while SELECT FOR UPDATE locks user records to prevent double-spending.
CREATE TABLE balances (
user_id UUID, currency TEXT,
available DECIMAL(28,18) NOT NULL DEFAULT 0 CHECK (available >= 0),
reserved DECIMAL(28,18) NOT NULL DEFAULT 0 CHECK (reserved >= 0),
PRIMARY KEY (user_id, currency)
);
CREATE TABLE orders (
order_id UUID PRIMARY KEY, user_id UUID NOT NULL,
pair TEXT NOT NULL, side TEXT NOT NULL,
order_type TEXT NOT NULL, price DECIMAL(28,18),
quantity DECIMAL(28,18) NOT NULL,
filled_qty DECIMAL(28,18) DEFAULT 0,
status TEXT DEFAULT 'open',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE trades (
trade_id UUID PRIMARY KEY, pair TEXT NOT NULL,
buyer_id UUID NOT NULL, seller_id UUID NOT NULL,
price DECIMAL(28,18) NOT NULL,
quantity DECIMAL(28,18) NOT NULL,
executed_at TIMESTAMPTZ DEFAULT NOW()
);Redis (Hot Balances and Order Book Cache)
HSET balance:{user_id} BTC_available "1.234" BTC_reserved "0.5"
# Atomic reservation via Lua script:
local avail = tonumber(redis.call('HGET', KEYS[1], ARGV[1]))
if avail >= tonumber(ARGV[2]) then
redis.call('HINCRBYFLOAT', KEYS[1], ARGV[1], -tonumber(ARGV[2]))
redis.call('HINCRBYFLOAT', KEYS[1], ARGV[3], tonumber(ARGV[2]))
return 1
end
return 0
# Order book snapshot cache
SET orderbook:BTC_USDT '{bids:[...],asks:[...]}' EX 1Fault Tolerance
Exchange Hack Prevention
1. Hot wallet = 5% of assets → limits blast radius 2. Multi-sig: hot=2-of-3, cold=3-of-5 → no single key compromise 3. HSM (Hardware Security Module) → keys never in software memory 4. Withdrawal anomaly detection → auto-freeze + human review 5. IP whitelisting for withdrawal addresses (user-configurable) 6. 24h withdrawal lock after password change 7. Circuit breaker: unusual trading volume → halt market 8. Proof of Reserves: Merkle tree of all balances → public audit
Additional Considerations
Proof of Reserves
A cryptographic Merkle tree aggregates account balances, where each leaf represents a hash of the user identifier and account balance. The root hash is published periodically, allowing users to verify that their balance is included in the tree without revealing other users' balances to the public. An independent auditor then confirms that total on-chain assets held in cold and hot wallets equal or exceed the Merkle root total.
Market Manipulation Detection
Wash trading occurs when the same entity buys and sells against themselves, which the system detects through IP, device fingerprint, and execution timing correlation. Spoofing occurs when large orders are placed and canceled quickly, detected via order lifetime analysis. A circuit breaker automatically halts market trading for a 5-minute cooldown whenever the price moves more than 10% within a 5-minute window.
Comparison with a Traditional Stock Exchange
Compared to a traditional Stock Exchange, a cryptocurrency exchange operates under fundamentally different settlement and custody models. Stock exchanges operate in heavily regulated environments with T+1 or T+2 settlement and central clearing houses (such as the DTCC) holding underlying assets, eliminating direct brokerage custody risk. In contrast, cryptocurrency exchanges manage direct self-custody or user keys, execute instant internal settlement, and support 24/7/365 multi-currency operations alongside on-chain deposits and withdrawals. Because the exchange holds private keys, hack prevention requires a strict hot and cold wallet architecture.
Interview Walkthrough
- 25-minute cut
Skip arch50/arch75 depth unless staff.
- In-memory matching engine per pair + WAL (8 min)
- Hot wallet ~5% of assets, and cold storage for the rest (9 min)
- Kafka bridges match to async settlement (8 min)
- Split the hot wallet (online trading) from the cold wallet (majority of funds offline) to establish security architecture first.
- Explain the order matching engine as single-threaded per trading pair with an in-memory order book.
- Cover the deposit and withdrawal pipeline with blockchain confirmation thresholds and ledger reconciliation.
- Discuss KYC and AML as gating steps before account activation, not inline on every trade.
- Mention idempotent order placement and balance locking during open orders.
- Common pitfall: keeping all funds in a hot wallet, where a single breach drains everything, making cold storage mandatory.
Engineering Trade-offs
Exchanges balance matching latency against settlement safety, contrasting fast in-memory books with a durable database-backed audit trail.
Matching Engine: In-Memory vs Database-Backed
Option 1: Database-backed (naive) Order book in PostgreSQL: SELECT top bid, ask WHERE bid >= ask ✓ Durable (ACID) ✗ Throughput: 10K TPS max (DB lock contention) ✗ Latency: 5-20ms per match Option 2: In-memory matching engine (Binance, Kraken) Bids: max-heap by price DESC. Asks: min-heap by price ASC. ✓ Microsecond latency, 1M+ orders/sec → State lost on crash Mitigation: WAL to Kafka + periodic snapshots to S3 Option 3: Hybrid (production-grade) In-memory for active order book PostgreSQL for trade records, order history Kafka as durable event log (source of truth for recovery)
Order Types Implementation
Market Order: Match immediately against best available asks.
Limit Order: If matching ask exists, execute; otherwise add to book.
Stop-Loss: Stored in separate stop order book; when triggered, converted to market order.
Regulatory Compliance: KYC/AML Architecture
KYC levels determine trading limits. AML: rules-based (structuring, pass-through, velocity) + ML layer (transaction graph analysis, pattern detection, network clustering). Suspicious Activity Report (SAR) filed with FinCEN. Travel Rule (FATF): share sender/receiver KYC for transfers > $3,000.
Review
How helpful was this walkthrough?
Click a star to rate. We actively use this feedback to refine and update our system design content.
Discussion
Share your thoughts, ask questions, or help others.