Real-Time Exchange Core
Limit Order Book Matching Engine
A Go matching engine with strict price-time priority, one lock-free order book per symbol, write-ahead logged to Redis Streams. 2,300 orders/sec sustained at 0.11 ms p50, and a byte-identical book after kill -9.
The problem
A matching engine is one of the few systems where correctness is not negotiable and latency is the product. Two orders at the same price must fill in the order they arrived, an acknowledged fill can never be lost, and a stale quote is a quote someone else picks off. The hard part isn't the matching algorithm — it's staying exact under concurrency, surviving a crash without inventing or losing state, and refusing work honestly when the durability layer is down.
Architecture & approach
Each symbol's order book is owned by exactly one goroutine; orders reach it through a buffered channel. The book itself contains no locks, no atomics, and no synchronization at all — nothing else can ever touch it. Reads go through the same channel for the same reason.
Prices are integer ticks, never floats: matching is driven by equality and ordering on price, so an approximate representation would make the engine's central decision approximate. Decimal strings are parsed to ticks once at the API boundary without ever constructing a float.
Time priority is a sequence number assigned by the single writer, not a wall-clock timestamp — clocks move backwards under NTP and collide at nanosecond resolution. A counter is a total order by construction, and reproducible during replay.
Price levels are a hash map plus a sorted linked list with a cached best pointer: O(1) best bid/ask, O(1) add at an existing level, O(1) teardown, and ordered iteration for sweeps and snapshots. New-level insert is O(k) from the touch — benchmarked adversarially at 5.7 µs to price the trade-off honestly.
The WAL logs commands, not outcomes, because matching is deterministic — replaying the same command sequence rebuilds the same book including queue positions. Writes are group-committed: one Redis round-trip amortized across every command that arrived while the previous batch was in flight, so the batch grows exactly when load rises.
Durability is fail-closed. When the WAL goes down the engine sheds writes with 503 rather than acknowledging fills it can't recover; /health flips to 503 so the load balancer pulls the instance. A 15s injected outage shed 12,010 orders cleanly while reads stayed 100% available at 1.48 ms p99, and intake recovered in 1 ms.
Market data is snapshot-then-delta over WebSocket with per-message sequence numbers and bounded per-client buffers — a consumer that can't keep up is disconnected rather than allowed to backpressure the match loop. 599,950 frames to 50 concurrent clients with zero sequence gaps.
The single-writer model was benchmarked against the obvious alternative rather than assumed: a per-book mutex is actually faster for synchronous request/reply (272 ns vs 904 ns at 8 symbols). The shard model earns its place on group-commit batching, symbol isolation, non-blocking reads, and deterministic replay ordering — not raw round-trip speed.
Stack
- Go
- Redis Streams
- WebSocket
- Next.js
- k6
- Fly.io
- Upstash
- Docker