Building a cryptocurrency exchange is one of the most challenging engineering feats. It combines the low-latency requirements of High-Frequency Trading (HFT) with the high-stakes security of a bank. A single bug in the matching engine or ledger can lead to insolvency.
In this guide, we will break down the architecture of a modern, institutional-grade crypto exchange with real code examples and production insights.
A robust exchange is composed of several decoupled microservices (or modular monoliths for latency):
1┌──────┐
2│ User │
3└───┬──┘
4 │ REST/WebSocket
5 ▼
6┌─────────┐
7│ Gateway │
8└────┬────┘
9 │ New Order
10 ▼
11┌─────────────┐
12│ Risk Engine │
13└──────┬──────┘
14 │ Valid
15 ▼
16┌──────────────────┐
17│ Matching Engine │
18└────┬─────────┬───┘
19 │ │ Order Fill
20 │ └──────────┐
21 │ Trade Executed │
22 ▼ ▼
23┌────────┐ ┌─────────┐
24│ Ledger │◄──────────┤ Gateway │
25└───▲────┘ Balance └─────────┘
26 │ Update
27 │ Credit
28┌───┴────────────┐
29│ Wallet Service │
30└───▲────────────┘
31 │ Deposit
32┌───┴────────┐
33│ Blockchain │
34└────────────┘
35The matching engine must be deterministic and fast. Here's a simplified implementation in Rust:
1use std::collections::BTreeMap;
2use std::cmp::Ordering;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Side {
6 Buy,
7 Sell,
8}
9
10#[derive(Debug, Clone)]
11pub struct Order {
12 pub id: u64,
13 pub side: Side,
14 pub price: u64, // Price in cents/satoshis
15 pub quantity: u64,
16 pub timestamp: u64,
17}
18
19pub struct OrderBook {
20 // Bids: sorted descending (highest price first)
21 bids: BTreeMap<u64, Vec<Order>>,
22 // Asks: sorted ascending (lowest price first)
23 asks: BTreeMap<u64, Vec<Order>>,
24}
25
26impl OrderBook {
27 pub fn new() -> Self {
28 OrderBook {
29 bids: BTreeMap::new(),
30 asks: BTreeMap::new(),
31 }
32 }
33
34 pub fn add_order(&mut self, order: Order) -> Vec<Trade> {
35 let mut trades = Vec::new();
36 let mut remaining_qty = order.quantity;
37
38 // Try to match against opposite side
39 match order.side {
40 Side::Buy => {
41 // Match against asks (sells)
42 while remaining_qty > 0 {
43 if let Some((&best_ask_price, _)) = self.asks.iter().next() {
44 if order.price >= best_ask_price {
45 // Can match!
46 let matched = self.match_order(
47 order.id,
48 best_ask_price,
49 &mut remaining_qty,
50 Side::Buy
51 );
52 trades.extend(matched);
53 } else {
54 break; // No more matches possible
55 }
56 } else {
57 break; // No asks available
58 }
59 }
60
61 // Add remaining to book
62 if remaining_qty > 0 {
63 let mut resting_order = order.clone();
64 resting_order.quantity = remaining_qty;
65 self.bids.entry(order.price)
66 .or_insert_with(Vec::new)
67 .push(resting_order);
68 }
69 }
70 Side::Sell => {
71 // Match against bids (buys)
72 while remaining_qty > 0 {
73 if let Some((&best_bid_price, _)) = self.bids.iter().next_back() {
74 if order.price <= best_bid_price {
75 let matched = self.match_order(
76 order.id,
77 best_bid_price,
78 &mut remaining_qty,
79 Side::Sell
80 );
81 trades.extend(matched);
82 } else {
83 break;
84 }
85 } else {
86 break;
87 }
88 }
89
90 if remaining_qty > 0 {
91 let mut resting_order = order.clone();
92 resting_order.quantity = remaining_qty;
93 self.asks.entry(order.price)
94 .or_insert_with(Vec::new)
95 .push(resting_order);
96 }
97 }
98 }
99
100 trades
101 }
102
103 fn match_order(
104 &mut self,
105 taker_id: u64,
106 price: u64,
107 remaining_qty: &mut u64,
108 side: Side
109 ) -> Vec<Trade> {
110 let mut trades = Vec::new();
111
112 let orders = match side {
113 Side::Buy => self.asks.get_mut(&price),
114 Side::Sell => self.bids.get_mut(&price),
115 };
116
117 if let Some(orders_at_price) = orders {
118 orders_at_price.retain_mut(|maker_order| {
119 if *remaining_qty == 0 {
120 return true; // Keep order
121 }
122
123 let trade_qty = (*remaining_qty).min(maker_order.quantity);
124
125 trades.push(Trade {
126 maker_order_id: maker_order.id,
127 taker_order_id: taker_id,
128 price,
129 quantity: trade_qty,
130 });
131
132 *remaining_qty -= trade_qty;
133 maker_order.quantity -= trade_qty;
134
135 maker_order.quantity > 0 // Keep if still has quantity
136 });
137 }
138
139 trades
140 }
141}
142
143#[derive(Debug)]
144pub struct Trade {
145 pub maker_order_id: u64,
146 pub taker_order_id: u64,
147 pub price: u64,
148 pub quantity: u64,
149}
150To ensure determinism and avoid race conditions, the core matching logic is often single-threaded:
The Risk Engine prevents users from trading money they don't have. It must run before the matching engine.
Critical Check: Pre-locking funds.
When a user places a BUY order for 1 BTC at 50,000 in their account. If the order is cancelled, the funds are released.
Latency Challenge: If the Risk Engine queries a SQL database for every order, your exchange will be slow.
This is where the "Double Spend" problem is solved internally.
Never just store balance = 100. Use double-entry bookkeeping. Every trade is a transaction:
SQL Schema:
1-- User accounts
2CREATE TABLE accounts (
3 account_id BIGSERIAL PRIMARY KEY,
4 user_id BIGINT NOT NULL,
5 asset VARCHAR(10) NOT NULL, -- 'USD', 'BTC', 'ETH', etc.
6 balance DECIMAL(20, 8) NOT NULL DEFAULT 0,
7 locked_balance DECIMAL(20, 8) NOT NULL DEFAULT 0,
8 version INT NOT NULL DEFAULT 0, -- For optimistic locking
9 UNIQUE(user_id, asset)
10);
11
12-- Journal entries (double-entry ledger)
13CREATE TABLE journal_entries (
14 entry_id BIGSERIAL PRIMARY KEY,
15 transaction_id BIGINT NOT NULL,
16 account_id BIGINT NOT NULL REFERENCES accounts(account_id),
17 debit DECIMAL(20, 8),
18 credit DECIMAL(20, 8),
19 asset VARCHAR(10) NOT NULL,
20 description TEXT,
21 created_at TIMESTAMP DEFAULT NOW(),
22 CHECK ((debit IS NULL AND credit IS NOT NULL) OR
23 (debit IS NOT NULL AND credit IS NULL))
24);
25
26-- Trades
27CREATE TABLE trades (
28 trade_id BIGSERIAL PRIMARY KEY,
29 buy_order_id BIGINT NOT NULL,
30 sell_order_id BIGINT NOT NULL,
31 buyer_user_id BIGINT NOT NULL,
32 seller_user_id BIGINT NOT NULL,
33 price DECIMAL(20, 8) NOT NULL,
34 quantity DECIMAL(20, 8) NOT NULL,
35 base_asset VARCHAR(10) NOT NULL, -- e.g., 'BTC'
36 quote_asset VARCHAR(10) NOT NULL, -- e.g., 'USD'
37 executed_at TIMESTAMP DEFAULT NOW()
38);
39Example Transaction (BTC/USD trade):
1-- Trade: Alice buys 1 BTC from Bob at $50,000
2BEGIN;
3
4-- 1. Debit Alice's USD
5INSERT INTO journal_entries (transaction_id, account_id, debit, asset, description)
6VALUES (12345, alice_usd_account, 50000.00, 'USD', 'Buy 1 BTC @ 50000');
7
8-- 2. Credit Bob's USD
9INSERT INTO journal_entries (transaction_id, account_id, credit, asset, description)
10VALUES (12345, bob_usd_account, 50000.00, 'USD', 'Sell 1 BTC @ 50000');
11
12-- 3. Debit Bob's BTC
13INSERT INTO journal_entries (transaction_id, account_id, debit, asset, description)
14VALUES (12345, bob_btc_account, 1.00000000, 'BTC', 'Sell 1 BTC @ 50000');
15
16-- 4. Credit Alice's BTC
17INSERT INTO journal_entries (transaction_id, account_id, credit, asset, description)
18VALUES (12345, alice_btc_account, 1.00000000, 'BTC', 'Buy 1 BTC @ 50000');
19
20-- 5. Update balances with optimistic locking
21UPDATE accounts
22SET balance = balance - 50000.00, version = version + 1
23WHERE account_id = alice_usd_account AND version = expected_version;
24
25UPDATE accounts
26SET balance = balance + 50000.00, version = version + 1
27WHERE account_id = bob_usd_account AND version = expected_version;
28
29-- ... (similar for BTC accounts)
30
31COMMIT;
32Real-world performance numbers from a production exchange:
| Component | Throughput | Latency (P99) | Technology |
|---|---|---|---|
| Matching Engine | 50,000 orders/sec | 200 μs | Rust, single-threaded |
| Risk Checks | 100,000 checks/sec | 50 μs | Redis cache |
| Database Writes | 10,000 trades/sec | 5 ms | PostgreSQL (batched) |
| WebSocket Updates | 200,000 msg/sec | 10 ms | Rust + Tokio |
Optimization Techniques:
Production exchanges require comprehensive monitoring:
1# Prometheus metrics example
2from prometheus_client import Counter, Histogram, Gauge
3
4# Order flow
5orders_received = Counter('exchange_orders_received_total', 'Total orders received', ['symbol'])
6orders_matched = Counter('exchange_orders_matched_total', 'Total orders matched', ['symbol'])
7orders_rejected = Counter('exchange_orders_rejected_total', 'Total orders rejected', ['reason'])
8
9# Latency
10matching_latency = Histogram('exchange_matching_latency_seconds', 'Matching engine latency')
11db_write_latency = Histogram('exchange_db_write_latency_seconds', 'Database write latency')
12
13# Book depth
14book_depth_bids = Gauge('exchange_book_depth_bids', 'Number of bid levels', ['symbol'])
15book_depth_asks = Gauge('exchange_book_depth_asks', 'Number of ask levels', ['symbol'])
16
17# Wallet health
18hot_wallet_balance = Gauge('exchange_hot_wallet_balance', 'Hot wallet balance', ['asset'])
19pending_withdrawals = Gauge('exchange_pending_withdrawals', 'Pending withdrawal count')
20best_bid >= best_ask (indicates matching engine bug)blockchain_balance != internal_ledger_balanceThe bridge to the blockchain is the most vulnerable point.
Reconciliation Process:
1async def reconcile_wallets():
2 """Run every 5 minutes."""
3 for asset in ['BTC', 'ETH', 'USDT']:
4 # Get blockchain balance
5 blockchain_balance = await blockchain_client.get_balance(asset)
6
7 # Get internal ledger balance
8 ledger_balance = await db.query(
9 "SELECT SUM(balance) FROM accounts WHERE asset = $1",
10 asset
11 )
12
13 discrepancy = abs(blockchain_balance - ledger_balance)
14
15 if discrepancy > TOLERANCE:
16 # CRITICAL: Halt withdrawals immediately
17 await circuit_breaker.open(f"wallet_{asset}")
18 await alert_team(
19 f"CRITICAL: {asset} balance mismatch! "
20 f"Blockchain: {blockchain_balance}, "
21 f"Ledger: {ledger_balance}"
22 )
23Any discrepancy triggers an immediate "Circuit Breaker" to halt withdrawals.
Building an exchange is less about "blockchain" and more about high-performance distributed systems and strict accounting. Speed gets users, but security keeps them.
Key Takeaways:
Start simple, test rigorously, and scale incrementally. The financial stakes are too high for shortcuts.
Technical Writer
NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.
Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.