Financial & Trading
Design Crypto Exchange
01
What the exchange must do
What the exchange must do
- Users place limit, market and stop orders on spot markets such as BTC-USDT or SOL-EUR, and cancel them.
- Orders match by price, then time; fills update both sides' balances immediately.
- Traders stream live order books, trades and candles, and bots trade through REST and WebSocket APIs.
- Each user gets a deposit address per network; incoming coins are credited after enough confirmations.
- Users withdraw to external addresses, with address allow-lists, two-factor checks and risk review.
- Identity checks, sanctions screening and limits by verification tier apply to deposits, trading and withdrawals.
- Order acknowledgement in under 5 ms at p99 inside the matching path; market data out within 20 ms.
- No fill may ever use funds that are not available: balances never go negative.
- No lost or duplicated trades after a crash; the engine recovers to the exact same book.
- Runs 24 hours a day, every day; upgrades happen without halting markets for more than seconds.
- Under 3% of customer assets sit in internet-connected wallets at any moment.
- Every balance change is traceable to an order, a trade, a deposit or a withdrawal.
Confirm spot only or derivatives too, whether fiat on-ramps are in scope, and how many networks you support. Futures add margin, liquidation and funding rates, which is a different interview. Most interviewers want spot trading plus custody, and they care most about how you keep money safe.
02
Sizing the exchange
Sizing the exchange
Assume 15M verified users, 1.5M of them active on a typical day, 400 markets, and 40 blockchains. Trading is very uneven: the top ten markets carry most of the volume, and market makers send far more orders than humans.
| Quantity | Working | Result |
|---|---|---|
| Orders on a normal day | mostly from about 3,000 API clients | about 2.5B |
| Average order rate | 2.5B / 86,400 s | about 29K / s |
| Spike order rate | about 8x average during a sharp price move | about 250K / s |
| Share on the busiest market | BTC-USDT alone in a spike | about 60K orders / s |
| Trades in a spike | roughly 1 fill per 12 orders | about 20K / s |
| Resting orders in memory | 400 markets, about 250K live orders each at most | up to 100M orders, under 20 GB |
| Journal written | 250K / s x about 120 B per event | about 30 MB / s at peak |
| Market data sockets | apps, bots and partners | about 1.8M connections |
| Book update fan-out | 50 book changes / s per hot market x 300K subscribers | about 15M messages / s before batching |
| Deposits to detect | 40 chains, about 2M customer credits a day | about 23 / s average |
| Withdrawals | about 600K a day | about 7 / s, each needing a signature |
Even at 250K orders a second, one market rarely needs more than about 60K a second, which a single CPU core can match. The heavy load is in risk checks and market data, which spread across many machines. Custody is low volume but carries most of the financial risk.
Confirmation rules vary a lot. Bitcoin is usually considered settled after several blocks, which can take close to an hour, while chains with fast finality settle in seconds. Credit timing is a policy per network, not one constant.
03
API design
API design
04
Architecture
Architecture
flowchart LR
U["Apps and trading bots"] --> GW["API gateway and rate limits"]
GW --> OR["Order service: validate and hold funds"]
OR --> SQ["Sequencer per market"]
SQ --> ME["Matching engine, one per market"]
ME --> J[("Replicated journal")]
J --> LG[("Double-entry ledger")]
J --> MD["Market data publisher"]
MD --> WS["WebSocket edge"]
WS --> U
CW["Chain watchers, one per network"] --> DP["Deposit crediting"]
DP --> LG
WD["Withdrawal service"] --> RK["Risk and sanctions screening"]
RK --> SG["Signing service: HSM or MPC"]
SG --> HW[("Hot wallets, small float")]Two halves meet at the ledger. On the trading side, the order service authenticates, validates the order and places a hold on the funds it could consume. Only then does the order go to the sequencer for its market, which stamps it with a sequence number and appends it to a replicated journal. The matching engine consumes the journal in order, updates its in-memory book and emits fills, which go back into the journal as events.
Downstream consumers read the journal and never talk to the engine directly. The ledger turns fills into double-entry postings and releases or converts holds. The market data publisher turns book changes into depth snapshots, trade ticks and candles. Because everything downstream is driven by the same ordered journal, any consumer can be rebuilt by replaying it.
On the custody side, a chain watcher per network runs its own nodes, follows new blocks, matches outputs to customer deposit addresses and waits for the network's confirmation rule before crediting. Withdrawals go the other way: screening, a policy check, a signature from keys held in hardware or split across parties, then broadcast and tracking until confirmed. Hot wallets hold only a small float; warm and cold wallets refill them through a slower, human-approved process.
Single-threaded, one per market, holding the whole book in memory. It does no I/O on the hot path except reading its input journal, which is why it is fast and deterministic.
An order can only reach the book after its maximum cost is placed on hold. The engine never checks balances, because the money is already set aside.
Append-only double-entry postings in a strongly consistent database. User balances are projections of the postings, and the sum of every asset across all accounts always nets to zero.
Hot for automatic withdrawals, warm for daily refills with multi-party approval, cold for the bulk of assets on offline hardware with people in different locations required to sign.
05
Deep dive: matching, holds and moving coins safely
Deep dive: matching, holds and moving coins safely
How do you match fast without losing anything in a crash? How do you guarantee no one trades money they do not have? And how do you take deposits and send withdrawals without crediting fake coins or losing real ones?
The matching engine
Each market gets one engine thread with the full order book in memory. Bids and asks are sorted maps keyed by price, each price level holds a FIFO queue of orders, and a hash map from order id to its queue node makes cancels constant time. A thread per market means no locks inside the engine, and since one market's orders never interact with another's, markets scale out by placing engines on different cores and hosts.
Determinism is the property that makes it safe. The engine reads inputs only from its journal, never consults the clock or random numbers, and never does network or disk I/O while matching. Given the same journal, it always produces the same fills. That gives you crash recovery (reload the last snapshot, replay the journal tail) and hot standby (a second engine on another host consumes the same journal and stays a few microseconds behind, ready to take over).
Prices and sizes are fixed-point integers internally, scaled by the market's tick size and lot size. Floating point is never acceptable for money: 0.1 plus 0.2 does not equal 0.3 in binary floating point, and an exchange cannot round a customer's balance by accident.
The journal and the sequencer
The sequencer for a market is the single writer of its journal. It assigns a monotonically increasing sequence number to every input (new order, cancel, admin halt) and replicates it to at least two other machines in different zones before the engine processes it. A Raft group per market or a primary with synchronous backups both work; what matters is that an input is durable on a quorum before the engine acts on it.
Outputs follow the same rule. Fills and book changes are written to the output journal, and only events in the journal are visible to the ledger and to traders. If the engine crashes after matching but before writing, the standby replays the same input and produces the same fill, so nothing is lost and nothing is doubled. Snapshots of the book every few minutes keep replay times short.
Holds make double-spending impossible
A user with 10,000 USDT can send two buy orders for 8,000 USDT each in the same millisecond, to two different markets. If each engine checked the balance itself, both could pass. The fix is to move the balance check before the engine. The order service atomically moves the order's maximum cost from available to on_hold in the user's balance row, conditional on available being high enough. The second order fails that condition and is rejected before it reaches any book.
Fills then convert holds into transfers: the buyer's held USDT goes to the seller, the seller's held ETH goes to the buyer, minus fees, all as one ledger transaction. Cancels and expired orders release what is left of the hold. For market orders with no price limit, hold the full quote amount the user specifies or a size-based cap with a protection price. Balance rows are sharded by user, so holds for different users never contend.
Crediting deposits without being fooled
Each network has a watcher service connected to the exchange's own full nodes, never only to third-party APIs. It follows the chain tip, decodes each block, and looks for outputs paying any of millions of deposit addresses, using a Bloom filter in front of the address table to keep the per-block check cheap. A match creates a pending deposit with zero confirmations.
Crediting waits for the network's rule. Proof-of-work chains need several blocks on top because a reorganisation can replace recent blocks and erase a transaction; chains with explicit finality can be credited when the block is finalized. If a reorg drops a pending deposit, the watcher marks it orphaned. For large amounts on weaker chains, require extra confirmations, and let users trade a credited-but-recent deposit while still blocking its withdrawal until it is deeply buried.
Tokens need care too. A transfer event from a fake contract with the same symbol is worthless. Watchers credit only events from an allow-listed contract address per asset, and they check the actual transfer amount, not what an event claims.
flowchart TD
R["Withdrawal request"] --> H["Hold funds in ledger"]
H --> C{"Screening and risk checks pass?"}
C -->|no| M["Manual review queue"]
C -->|yes| P{"Amount under hot wallet limit?"}
P -->|yes| S["Sign with hot key in HSM or MPC"]
P -->|no| W["Batch for warm wallet with approvals"]
W --> S
S --> B["Broadcast and track"]
B --> F["Final: convert hold into debit"]Sending withdrawals and protecting keys
A withdrawal first places a hold, then runs checks: new device or new address, an address book cooling-off period after changes, velocity limits, sanctions screening of the destination, and comparison with the user's normal behaviour. Requests that pass go to the signing service, which holds keys in HSMs or splits them with multi-party computation so no single machine ever has the full key. Policy limits live inside the signer itself, so even a compromised application server cannot sign a transfer above the hourly cap.
Hot wallets carry only enough to cover a few hours of normal withdrawals. A refill job tops them up from warm wallets with multi-person approval, and warm wallets are refilled from cold storage on a slower schedule. If the hot wallet is drained, the loss is bounded to that float. Exchanges also publish proof of reserves: a Merkle tree of customer balances, where each user can verify their own leaf, plus signed on-chain evidence that the exchange controls enough assets to cover the total.
06
Tradeoffs and design choices
Tradeoffs and design choices
No locks, deterministic replay, and microsecond matching. One market is capped by one core, which is fine for spot markets, and a hot market cannot borrow capacity from a quiet one.
Fast and auditableTransactions give correctness for free. Row locks on the best price level serialise everything anyway, and latency lands in milliseconds. A good starting point for an MVP only.
Too slow at volumeOverspending is impossible by construction, and engines stay ignorant of balances. Each order costs one conditional balance update, and holds must be released carefully on cancel and expiry.
Correct by designFewer moving parts per order, but every engine would need a consistent view of balances shared across markets. That reintroduces cross-market locking or races.
Invites double-spendA breach of online systems can only lose a few percent of assets. Large withdrawals wait for warm wallet batches, which can take an hour or more.
Bounded lossesEvery withdrawal is instant. Every coin is one key compromise away from gone. History is full of exchanges that learned this the hard way.
Unacceptable riskUsers on fast-finality chains get credited in seconds; Bitcoin deposits wait for several blocks. The trade is user patience against reorg risk, set per network and per amount.
Safe and tunableEasy to explain, wrong for every chain: too slow for some and unsafe for others.
Ignores chain differences07
Failure modes
Failure modes
An attacker gains access to signing infrastructure and starts sending coins out as fast as the network allows.
Keep keys in HSMs or MPC with limits enforced inside the signer. Alert on any withdrawal not linked to a ledger request, freeze the affected wallet automatically, and cap total exposure by keeping the hot float small.A deep reorganisation on a smaller proof-of-work chain removes a deposit that the exchange already credited, and the user has already traded and withdrawn the proceeds in another coin.
Use higher confirmation counts for weaker chains and for large amounts. Allow trading a recent deposit but block withdrawals until it is buried deeply, and monitor hash rate so you can pause a chain under attack.Several thousand orders were accepted, some partly matched. Traders do not know which fills happened.
Promote the hot standby, which has already consumed the same journal. Inputs were durable before matching, outputs are deterministic, and market data resumes with sequence numbers so clients can request a fresh snapshot for any gap.A large market sell on a low-liquidity pair eats through the book and prints trades 40% below the last price, triggering stop orders that make it worse.
Apply price bands that reject orders too far from a reference price, add a short volatility pause, cap market order slippage, and protect stop triggers with a mark price rather than the last trade.A watcher bug misses a sweep transaction, so the exchange believes a wallet holds more than it does.
Reconcile every wallet against the chain every hour, block withdrawals from any wallet that is off, and treat the gap as an incident until the cause is found.A deposit traces back to an address on a sanctions list. Crediting it may expose the exchange to legal penalties.
Screen deposit sources before crediting, freeze matching funds in a quarantine account, file the required report, and record the decision path.08
Interview tips
Interview tips
Split the problem in two out loud. Say there is a trading core and a custody system joined by a ledger. Interviewers often care more about custody than matching.
Make the engine deterministic. Single thread per market, journal-driven, no I/O while matching. Then crash recovery and standbys fall out of replay.
Move the balance check before the book. Explaining holds as a conditional update is the cleanest answer to double-spending across markets.
Know finality differs by chain. Mention reorgs, confirmation counts per network and allow-listed token contracts. It shows you understand blockchains beyond the buzzwords.
Bound the blast radius of a breach. Hot, warm and cold tiers, limits inside the signer, and MPC or HSM keys. Put a number on the hot float.
Never use floats. Say fixed-point integers per market. It takes five seconds and interviewers notice when it is missing.
Mention proof of reserves. Merkle trees of balances plus on-chain evidence are now standard and show awareness of what users expect after recent collapses.
09
Evolution
Evolution
Orders match in a Postgres transaction, deposits are polled from a third-party API. Fine for a demo, dangerous with real money.
Engines move to memory with a journal; most coins move to offline storage, and holds are added before matching.
Each network gets nodes and a watcher with its own finality rule. Hot wallets refill from warm storage under approval policies.
Identity tiers, sanctions screening on deposits and withdrawals, travel rule messages between exchanges and full audit trails.
Regular Merkle-tree attestations, MPC custody, staking and derivatives, each reusing the ledger and journal.
Watch and read
Try next
Free to read · better with landed
Whiteboard this with landed
landed runs it as a live system design round on the whiteboard and grades your trade-offs.
