Low Level Design: Crypto Exchange Order Book

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
How does a crypto exchange order book work?

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.

What is price-time priority matching in an order book?

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.

How does settlement differ between traditional and crypto exchanges?

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.

How do you prevent wash trading and market manipulation on a crypto exchange?

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

Scroll to Top