Problem Statement
Design the order book engine for a crypto exchange: handle limit and market orders, match them using price-time priority, settle trades, aggregate depth, and enforce risk controls.
Core Requirements
- Place, cancel, and query limit and market orders
- Price-time priority matching engine
- Trade settlement with fee deduction
- Order book depth (L2) aggregation
- Risk controls: position limits, self-trade prevention
Data Model
accounts
id BIGINT PK
user_id BIGINT FK -> users.id
currency VARCHAR(10)
available DECIMAL(28,10) NOT NULL DEFAULT 0
reserved DECIMAL(28,10) NOT NULL DEFAULT 0
orders
id BIGINT PK
user_id BIGINT FK -> users.id
pair VARCHAR(20) NOT NULL -- e.g. BTC-USD
side ENUM('buy','sell')
type ENUM('limit','market')
price DECIMAL(28,10) -- NULL for market orders
quantity DECIMAL(28,10) NOT NULL
filled DECIMAL(28,10) NOT NULL DEFAULT 0
status ENUM('open','partial','filled','cancelled')
time_in_force ENUM('GTC','IOC','FOK') DEFAULT 'GTC'
created_at TIMESTAMP
updated_at TIMESTAMP
trades
id BIGINT PK
pair VARCHAR(20) NOT NULL
maker_order_id BIGINT FK -> orders.id
taker_order_id BIGINT FK -> orders.id
price DECIMAL(28,10) NOT NULL
quantity DECIMAL(28,10) NOT NULL
maker_fee DECIMAL(28,10)
taker_fee DECIMAL(28,10)
executed_at TIMESTAMP
In-Memory Order Book Structure
The matching engine keeps the live order book in memory for microsecond-latency matching. Persistence is append-only to a write-ahead log (WAL); the in-memory book is rebuilt from the WAL on restart.
// Per trading pair
OrderBook {
pair: string
bids: SortedMap<price DESC, Queue<Order>> // buy orders, best bid first
asks: SortedMap<price ASC, Queue<Order>> // sell orders, best ask first
}
// Each price level holds a FIFO queue of orders
PriceLevel {
price: Decimal
orders: LinkedList<Order> // front = oldest = highest priority
total_qty: Decimal
}
Matching Algorithm (Price-Time Priority)
function match(incoming_order):
opposite_side = incoming_order.side == BUY ? asks : bids
while incoming_order.remaining_qty > 0 and opposite_side not empty:
best_level = opposite_side.peek()
if incoming_order.type == LIMIT:
if BUY and incoming_order.price < best_level.price: break
if SELL and incoming_order.price > best_level.price: break
resting_order = best_level.orders.front()
fill_qty = min(incoming_order.remaining_qty, resting_order.remaining_qty)
execute_trade(resting_order, incoming_order, best_level.price, fill_qty)
if resting_order.remaining_qty == 0: remove from book
if best_level.total_qty == 0: remove price level
if incoming_order.remaining_qty > 0 and incoming_order.type == LIMIT:
insert incoming_order into book at its price level (FIFO)
Trade Settlement
Settlement updates account balances atomically. For a BUY fill: decrease buyer reserved quote currency, increase buyer base currency, increase seller base reserved (release), increase seller quote currency. Fees are deducted from the received asset.
BEGIN TRANSACTION;
-- release buyer reserve, credit base
UPDATE accounts SET reserved = reserved - :cost, available = available + :fill_qty - :taker_fee
WHERE user_id = :buyer_id AND currency = :base;
UPDATE accounts SET available = available - :cost WHERE user_id = :buyer_id AND currency = :quote;
-- release seller reserve, credit quote
UPDATE accounts SET reserved = reserved - :fill_qty
WHERE user_id = :seller_id AND currency = :base;
UPDATE accounts SET available = available + :cost - :maker_fee
WHERE user_id = :seller_id AND currency = :quote;
INSERT INTO trades (...) VALUES (...);
UPDATE orders SET filled = filled + :fill_qty ... WHERE id IN (:maker_id, :taker_id);
COMMIT;
Order Book Depth Aggregation
L2 depth snapshots aggregate quantities at each price level. Clients subscribe via WebSocket; the engine publishes incremental diff events after each match. A snapshot endpoint provides the current state for reconnecting clients.
GET /orderbook/{pair}?depth=20
Response:
{
"bids": [[price, qty], ...], // top 20 bids descending
"asks": [[price, qty], ...], // top 20 asks ascending
"sequence": 4821039
}
WebSocket event: { "type": "diff", "pair": "BTC-USD", "sequence": 4821040,
"bids": [[42100.00, 0.5]], "asks": [[42105.00, 0]] }
Risk Controls
- Pre-trade balance check: reserve funds before inserting order; reject if insufficient available balance
- Self-trade prevention: if incoming and resting order share the same user_id, cancel the resting order (or the incoming, per policy) instead of filling
- Position limits: cap maximum open order quantity per user per pair
- Price bands: reject limit orders priced more than N% away from last trade price
- Circuit breaker: halt matching if price moves beyond threshold in a short window
API Design
POST /orders Place a new order
DELETE /orders/{orderId} Cancel an open order
GET /orders/{orderId} Get order status
GET /users/{userId}/orders Open and historical orders
GET /orderbook/{pair} L2 depth snapshot
GET /trades/{pair} Recent public trades
WS /stream/{pair} Real-time depth diffs and trade feed
Scaling Considerations
- One matching engine process per trading pair — eliminates cross-pair locking
- Engine publishes trade events to Kafka; settlement workers consume and update DB
- Order book state snapshotted to Redis periodically for fast client bootstrapping
- WAL replication to a standby engine for failover with sub-second RTO
- Separate read path for order history and depth queries from the hot matching path
Interview Tips
- Draw the price level data structure first — it anchors the whole discussion
- Explain why matching is single-threaded per pair: simplicity and determinism outweigh parallelism gains at this granularity
- Discuss time-in-force semantics: GTC rests, IOC cancels unfilled remainder immediately, FOK cancels entirely if not fully fillable
- Bring up self-trade prevention — interviewers rarely expect it and it signals real exchange knowledge
An order book is a real-time list of open buy (bid) and sell (ask) orders for a trading pair, sorted by price. Bids are ranked highest-price-first and asks are ranked lowest-price-first. When a new order arrives the matching engine checks whether the best opposing quote satisfies the order’s price constraint. If a match exists the engine executes a trade and removes the filled quantity from both sides; any unfilled remainder rests in the book as a limit order. The spread — the gap between the best bid and best ask — reflects current market liquidity.
Price-time priority (also called FIFO matching) is the standard algorithm used by most exchanges. Orders are first ranked by price: better-priced orders execute before worse-priced ones. Among orders at the same price level, the order that arrived earliest gets filled first. In practice the order book maintains a sorted map keyed by price, where each price level holds a FIFO queue of resting orders. This design rewards aggressive pricing and early order submission, and it is straightforward to implement with a balanced BST (e.g., a red-black tree or Java’s TreeMap) combined with per-level linked queues.
Traditional equity exchanges (e.g., NYSE) settle trades on a T+1 or T+2 cycle, meaning ownership and cash change hands one or two business days after the trade. Crypto exchanges typically settle in real time or within the next block confirmation (seconds to minutes) because the exchange holds user assets in custody and can update internal ledger balances immediately without waiting for external clearinghouses. On-chain settlement (withdrawal to a self-custody wallet) adds the latency of block confirmations and network fees, but exchange-internal transfers are instant balance adjustments with no counterparty risk window.
Wash trading — executing buy and sell orders from accounts you control to inflate volume — is detected by linking accounts through shared KYC data, IP addresses, device fingerprints, and deposit/withdrawal graph analysis. The exchange flags order pairs where the same beneficial owner appears on both sides of a trade. Additional controls include rate-limiting order cancellation (to deter spoofing), monitoring for layering patterns (large orders placed and rapidly cancelled to move the price), and applying circuit breakers that halt trading when price moves exceed a threshold in a short window. Suspicious activity is reported to compliance teams and, where required, to regulators.
See also: Coinbase Interview Guide
See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems
See also: Meta Interview Guide 2026: Facebook, Instagram, WhatsApp Engineering