System Design Interview: Design a Cryptocurrency Exchange (Coinbase / Binance)
A cryptocurrency exchange enables users to buy, sell, and trade digital assets like Bitcoin and Ethereum. The core challenge is matching buy and sell orders with extreme reliability and low latency, while maintaining a correct ledger of all balances. This guide covers the architecture used by Coinbase, Binance, and Kraken.
Requirements
Functional: deposit and withdraw fiat/crypto, place market and limit orders, execute order matching, display real-time order book and trade history, manage wallet balances, support advanced order types (stop-loss, stop-limit).
Non-functional: no lost trades (exactly-once execution), sub-millisecond matching latency, 100K orders/second throughput, auditable ledger, regulatory compliance.
Order Book
The order book is a real-time list of all open buy (bid) and sell (ask) orders for a trading pair (BTC/USD):
Order Book BTC/USD:
ASKS (sell orders, ascending price):
50,001.00 | 0.50 BTC | 3 orders
50,000.50 | 1.20 BTC | 1 order
50,000.00 | 2.00 BTC | 5 orders ← best ask (lowest sell price)
BIDS (buy orders, descending price):
49,999.00 | 1.50 BTC | 2 orders ← best bid (highest buy price)
49,998.50 | 0.80 BTC | 1 order
49,998.00 | 3.00 BTC | 4 orders
Spread = best ask - best bid = 50,000.00 - 49,999.00 = $1.00
Matching Engine
The matching engine is the core of the exchange. It processes incoming orders and matches them against existing orders in the book:
# Data structure: two sorted sets per trading pair
# Bids: max-heap by price (best bid = highest price at top)
# Asks: min-heap by price (best ask = lowest price at top)
def process_order(order):
if order.type == MARKET:
# Fill against best available price
while order.quantity > 0 and asks:
best_ask = asks.peek()
if order.side == BUY:
fill = min(order.quantity, best_ask.quantity)
execute_trade(order, best_ask, fill)
order.quantity -= fill
best_ask.quantity -= fill
if best_ask.quantity == 0: asks.pop()
elif order.type == LIMIT:
if order.side == BUY and asks and order.price >= asks.peek().price:
# Can match immediately
process_as_market(order)
else:
# Add to order book
bids.push(order) # max-heap by price, FIFO within same price
Price-time priority (FIFO): within the same price level, earlier orders are filled first. This is the standard fairness rule at most exchanges.
Matching Engine Architecture
The matching engine is the most latency-sensitive component:
- Single-threaded: the matching engine processes one order at a time to avoid concurrency complexity. No locks needed — sequential processing guarantees consistency.
- In-memory: the order book lives entirely in memory. No database I/O in the critical path. Throughput: 1-5M orders/second on modern hardware.
- Event sourcing: every order and trade is appended to an immutable event log (Kafka). The in-memory order book is rebuilt by replaying the log on startup. The log is the source of truth.
- One engine per trading pair: BTC/USD and ETH/USD have separate matching engines. Enables horizontal scaling without cross-pair coordination.
Wallet and Ledger
-- Double-entry ledger (append-only):
CREATE TABLE ledger (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
asset VARCHAR(10) NOT NULL, -- BTC, ETH, USD
amount NUMERIC(28, 8) NOT NULL, -- signed: positive=credit, negative=debit
type ENUM('deposit','withdrawal','trade','fee'),
ref_id UUID, -- trade_id or withdrawal_id
created_at TIMESTAMP DEFAULT NOW()
);
-- NEVER UPDATE or DELETE — only INSERT
-- Balance = SUM(amount) WHERE user_id = ? AND asset = ?
-- Reserve funds before order (prevent double-spend):
INSERT INTO ledger (user_id, asset, amount, type, ref_id)
VALUES (user_id, 'USD', -100.00, 'reserve', order_id);
-- If order fills: INSERT debit for USD, INSERT credit for BTC
-- If order cancelled: INSERT reverse credit for USD
Using NUMERIC(28, 8) for amounts avoids floating-point rounding. Balances are always computed as sum of ledger entries — never stored as a mutable balance column.
Order Placement Flow
1. User places BUY 1 BTC at $50,000 LIMIT
2. API validates: user has $50,000 USD available
3. Reserve $50,000 in ledger (debit reserve entry)
4. Publish order to Kafka → Matching Engine consumes
5. Matching Engine: order matches against ASK at $49,999
6. Execute trade:
- Publish TradeExecuted event to Kafka
- Ledger Service: debit $49,999 USD, credit 1 BTC to buyer
- Ledger Service: credit $49,999 USD, debit 1 BTC to seller
- Release any unfilled reserve ($1 remaining from $50,000)
7. WebSocket push: order book update + trade ticker to all connected clients
Real-Time Market Data
Clients need live order book updates and trade tickers:
- Matching engine publishes order book changes and trades to Kafka
- Market data service consumes Kafka, maintains WebSocket connections to clients
- Clients subscribe to specific trading pairs:
subscribe {pair: "BTC/USD", channels: ["orderbook", "trades"]} - Order book updates are aggregated (100ms batches) to avoid overwhelming clients during high volatility
Interview Tips
- The single-threaded matching engine surprises many candidates — explain why it is the correct choice
- Event sourcing (Kafka log as source of truth) is critical for auditability and crash recovery
- Use NUMERIC not FLOAT for money — especially important in crypto where 8 decimal places matter (0.00000001 BTC = 1 satoshi)
- The double-entry ledger pattern — mention it by name and explain why no balance column is stored
- Fund reservation before order placement prevents users from placing orders they cannot afford
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “Why does a crypto exchange use a single-threaded matching engine?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “A matching engine processes buy/sell orders against an order book. Using a single thread eliminates all locking overhead and race conditions — no mutex, no CAS loops, no concurrent modification. A single high-performance thread on a modern CPU can process 500K–1M orders/second, which exceeds the needs of most exchanges. The event-sourcing model (all orders arrive via a Kafka partition, processed in order) provides a total ordering guarantee. Multi-threading would require lock-based order book access which adds latency and complexity without meaningful throughput gains at typical exchange volumes.” }
},
{
“@type”: “Question”,
“name”: “What data structure should you use for an order book and why?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “Use a sorted map (TreeMap in Java, std::map in C++, SortedDict in Python) keyed by price, where each price level maps to a queue of orders at that price. Bids are sorted descending (highest first), asks ascending (lowest first). This gives O(log N) insert/delete and O(1) best bid/ask. For ultra-low latency (HFT), use a price-indexed array (fixed-size array indexed by price*100 for penny increments) for O(1) access, but this only works for instruments with bounded price ranges. For interview purposes, the sorted map approach is the correct answer.” }
},
{
“@type”: “Question”,
“name”: “How does a double-entry ledger work in a crypto exchange?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “Every financial movement creates two entries that sum to zero: a debit from one account and a credit to another. Example: user deposits 1 BTC → debit Exchange Custody account, credit User Balance account. A trade (buy 0.5 BTC at $50K) → debit Buyer USD balance, credit Seller USD balance; debit Seller BTC balance, credit Buyer BTC balance. Use NUMERIC(20,8) not FLOAT to avoid floating point errors. The invariant: SUM of all ledger entries = 0 at all times. This makes auditing trivial — if it doesn't sum to zero, there's a bug or fraud.” }
}
]
}