The matching engine is the smallest and most consequential component of a cryptocurrency exchange. It is typically a few thousand lines of code, and it decides whether every trade on your platform is correct. This is how one is actually built.
1. What the Engine's Job Actually Is
Narrower than most people assume. A matching engine takes a strictly ordered sequence of order events and produces a strictly ordered sequence of trade and book-update events. That is all.
What it must not do:
- Talk to a database.
- Check balances — risk checks belong in the gateway, upstream.
- Read the system clock. Time arrives as a field on the input event.
- Perform I/O of any kind in the hot path.
- Use floating point for prices or quantities.
The architectural discipline: the engine is a pure function over an ordered input stream. Every constraint above exists to preserve that property, and preserving it is what makes the system auditable, testable and recoverable.
2. Order Book Data Structures
A limit order book is two sorted collections of price levels — bids descending, asks ascending — where each level holds a FIFO queue of resting orders.
| Structure | Best price | Insert | Cancel | Verdict |
|---|---|---|---|---|
| Sorted array of levels | O(1) | O(n) shift | O(1) via index | Poor for wide books |
| Balanced tree (red-black) | O(log n) | O(log n) | O(log n) | Safe general default |
| Skip list | O(1) cached | O(log n) | O(log n) | Simpler concurrency story |
| Flat array by price tick | O(1) | O(1) | O(1) | Best for liquid pairs |
Why the flat array wins in practice
Real trading concentrates in a narrow band around the mid price. Allocate a fixed array indexed by price tick covering that band, keep a pointer to the best bid and best ask, and every hot-path operation becomes a direct index. You trade memory for constant-time access and near-perfect cache locality — and on modern hardware cache behaviour dominates asymptotic complexity at these scales. Orders far outside the band fall through to an overflow tree.
The three structures you need
- Price level index — flat array or tree, giving you the best bid/ask instantly.
- Per-level FIFO queue — an intrusive doubly-linked list so removal is O(1) given a node.
- Order ID map — hash map from order ID to its list node, so cancellation never scans.
Use fixed-point integers everywhere. Represent price and quantity as integers scaled by a per-market factor — for example price in units of 10-8. Floating point in a matching engine produces fills that are off by one satoshi, which reconciles to a discrepancy nobody can explain three weeks later.
3. The Matching Algorithm
Standard price–time priority. For an incoming buy limit order:
- Look at the best ask. If it does not exist, or its price is above the buy's limit price, stop — nothing crosses.
- Take the first order in that level's FIFO queue.
- Fill quantity = min(remaining incoming, resting order remaining).
- Execute at the resting order's price, never the incoming order's. The passive side sets the price; this is what makes maker rebates meaningful.
- Emit a trade event. Decrement both sides.
- If the resting order is fully filled, remove it from the queue and the ID map.
- Repeat from step 1 until the incoming order is exhausted or nothing crosses.
- Any remainder rests on the book at its limit price, joining the back of that level's queue.
Self-trade prevention
Before executing, compare account IDs. If they match, apply the configured policy — cancel the incoming order, cancel the resting order, or decrement both by the overlapping quantity. Exchanges without this control publish wash trades by accident and then have to explain them to a regulator.
4. Order Types
| Type | Behaviour | Engine complexity |
|---|---|---|
| Limit | Match up to limit price, rest the remainder | Baseline |
| Market | Match until filled or book exhausted | Low — needs slippage protection |
| Immediate-or-cancel (IOC) | Match what it can, cancel the rest | Low |
| Fill-or-kill (FOK) | All or nothing | Medium — requires a dry-run pass |
| Post-only | Reject if it would take liquidity | Low |
| Stop / stop-limit | Activates when a trigger price prints | Medium — needs a trigger book |
| Iceberg | Displays a slice, refills on fill | High — refill re-queues at the back |
Stop orders are worth a note: they do not live in the order book. They live in a separate trigger structure keyed by price, and when a trade prints through a trigger level the engine converts them into ordinary orders and feeds them back through the same input path — preserving determinism.
5. Determinism and Event Sourcing
The engine sits between two logs.
Input log: every accepted command — place, cancel, modify — with a monotonic sequence number and an ingress timestamp assigned by the gateway. Durably written before the engine sees it.
Output log: every trade and book delta, also sequenced. The ledger, market data publisher, reporting pipeline and risk system are all consumers of this log. None of them are in the engine's path.
This buys you four things at once:
- Recovery — snapshot plus replay reconstructs exact state.
- Auditability — you can prove precisely why a fill happened at a given price.
- Testing — capture a day of production input, replay it against a new engine build, and diff the output byte for byte.
- Time travel — replay to any sequence number to investigate a dispute.
The determinism checklist
- No wall-clock reads — time is an input field.
- No random number generation.
- No iteration over unordered maps in ways that affect output.
- No floating point arithmetic.
- No concurrency within a single book.
- No dependency on memory addresses or allocation order.
6. Failover and Recovery
The standard design is primary/standby over a replicated log:
- Gateway sequences an order and writes it to a replicated log (Kafka, Redpanda, or Raft-backed).
- Primary engine consumes, matches, emits output events.
- Standby engine consumes the same input log and maintains identical state — but suppresses output.
- On primary failure, the standby is already at the last committed sequence number and begins emitting.
- Recovery time is bounded by failure detection, not by state rebuild.
Snapshot the full book periodically — every few million events — so a cold start replays minutes rather than days.
Test the failover on a schedule. An untested standby is a standby that does not work. Run a controlled failover in production during low volume, monthly. Teams that do this discover the problems on a Tuesday afternoon instead of during a volatility spike.
7. Scaling and Performance
Scale by sharding pairs, not by threading a book.
| Stage | Throughput per pair | p99 latency | Architecture |
|---|---|---|---|
| Launch | 1k–5k orders/s | < 10 ms | Go/Java, single node + standby |
| Growth | 20k–100k orders/s | < 1 ms | Sharded pairs, pre-allocated memory, GC tuning |
| Tier-1 | 500k+ orders/s | < 100 µs | C++/Rust, lock-free ring buffer, pinned cores, kernel bypass |
Optimisations that actually pay, in order
- Pre-allocate everything. Object pools for orders and book nodes. Zero allocation in the hot path.
- Batch the output. Writing events in batches of 100 rather than individually is often a 10× throughput improvement for a few hundred microseconds of added latency.
- Pin the engine thread to a core and isolate that core from the scheduler.
- Flat arrays over pointer-chasing for the active price band.
- Kernel bypass networking — last, and only when you have measured that the kernel is the bottleneck.
8. Pitfalls That Reach Production
- Floating point money. Found in production by the trader who benefits from it.
- Wrong execution price. Filling at the aggressor's price instead of the resting order's. Silently overcharges takers.
- Cancel-fill race. A cancel arriving concurrently with a fill must be resolved by sequence number, not by timing. Otherwise a user is filled on an order they successfully cancelled.
- Iceberg refill priority. A refilled slice must join the back of the queue. Refilling at the front is a hidden priority advantage and a market-fairness violation.
- Unbounded market orders. A market order with no slippage cap will walk a thin book to zero. Always cap.
- Non-deterministic map iteration. Breaks replay in a way that is extremely painful to diagnose.
- Missing self-trade prevention. Publishes wash trades and creates a regulatory problem.
For how the engine fits into the wider platform — ledger, custody, compliance — see the complete cryptocurrency exchange development guide.
Frequently Asked Questions
What data structure should a limit order book use?
An array or map of price levels, each holding an intrusive doubly-linked list of orders in arrival order, plus a hash map from order ID to node for O(1) cancellation. For liquid pairs a flat array indexed by price tick outperforms a tree, because the active price range is narrow and cache locality dominates asymptotic complexity.
Why must a matching engine be deterministic?
Because recovery, auditing and testing all depend on replay. If replaying the same input sequence can produce different fills, you cannot rebuild state after a crash, cannot prove to a regulator what happened, and cannot write a meaningful test.
Should the matching engine be multi-threaded?
Not within a single order book. Locking or lock-free coordination inside a book buys microseconds and costs correctness and determinism. Scale horizontally by assigning different trading pairs to different engine instances.
How do you handle failover without losing orders?
Sequence and durably persist every accepted order to a replicated log before matching. A hot standby consumes the same log and maintains identical state. On failure the standby has already replayed everything up to the last committed sequence number and takes over.
What latency should a new exchange target?
Under 10ms p99 end-to-end is comfortably sufficient for a launching exchange with no professional market makers. Chasing microseconds before you have order flow is the most common way exchange projects waste a quarter.
Building a Trading Engine?
We design and implement deterministic matching engines and the event-sourced ledgers behind them. Let's talk architecture.
Talk to Our Blockchain Team