An order book is a data structure that maintains the set of outstanding buy and sell orders at each price level. For an FX market maker, maintaining a locally consistent view of the book at each LP (liquidity provider) is the foundation everything else sits on. At 100,000 book updates per second across 50 currency pairs, the implementation choices matter.
What an Order Book Contains
EUR/USD Order Book
BIDS (buy orders, descending price) OFFERS (sell orders, ascending price)
───────────────────────────────────── ──────────────────────────────────────
Price | Qty | Orders Price | Qty | Orders
───────────┼────────────┼────────── ───────────┼────────────┼──────────
1.08450 | 5,000,000 | 3 1.08452 | 3,000,000 | 2
1.08448 | 8,000,000 | 5 1.08455 | 7,000,000 | 4
1.08445 | 2,000,000 | 2 1.08460 | 4,000,000 | 3
1.08440 | 12,000,000 | 7 1.08465 | 9,000,000 | 5
... ...
↑
Spread = best offer - best bid
= 1.08452 - 1.08450 = 0.2 pips
Operations needed:
- Add a new order at a price level (create the level if it doesn’t exist)
- Delete an order (remove the level if it becomes empty)
- Modify an order quantity
- Get best bid / best offer (top of book)
- Iterate levels in price order (for market data consumers)
Updates arrive as incremental FIX X (Market Data Incremental Refresh) messages — one operation at a time. The book must be correct after each update.
Data Structure Options
TreeMap
TreeMap<BigDecimal, Level> is the natural Java choice. Prices as keys, level data as values, natural ordering gives bids (reverse) and offers (ascending) for free.
| |
Performance profile:
| Operation | TreeMap complexity | Actual cost |
|---|---|---|
| Add level | O(log n) | ~200ns (tree rebalance, allocation) |
| Delete level | O(log n) | ~180ns (tree rebalance) |
| Update quantity | O(log n) + O(1) | ~120ns (lookup + field write) |
| Best bid/offer | O(1) | ~20ns |
| Iterate top N | O(N) | ~50ns/level |
The problems:
- Allocation: every new price level allocates a
TreeMap.Entrynode on the heap. At 50k updates/second with frequent level creation/deletion, this is meaningful GC pressure. - Cache unfriendliness: tree nodes are scattered across the heap. Traversing even 5 levels for a depth-5 snapshot requires pointer-chasing through non-adjacent memory.
BigDecimalfor prices adds overhead — comparison and hashing are more expensive than primitive comparison.
Price-as-Integer Encoding
FX prices have at most 5 decimal places (pips). Storing the price as a long integer (price × 100,000) eliminates BigDecimal overhead entirely.
| |
Long key comparisons are ~5x faster than BigDecimal. No risk of floating-point representation errors. The book stays consistent because we’re in integer space.
Array-Based Book (Fixed Price Range)
For a known price range, an array indexed by price-as-integer eliminates the tree entirely:
| |
Performance profile:
| Operation | Array complexity | Actual cost |
|---|---|---|
| Add/update level | O(1) | ~15ns |
| Delete level | O(1) | ~10ns |
| Best bid/offer | O(1) with cached pointer | ~5ns |
| Iterate top N | O(N) | ~10ns/level (sequential memory) |
The sequential memory access pattern means the CPU’s prefetcher does useful work. The top 5 bid levels are adjacent in memory; iterating them fits in 2–3 cache lines.
The constraints: the array must be large enough to cover the expected price range, and the mid-price anchor must be updated when the market moves significantly (not hard — a 20-pip move triggers re-anchoring). Pre-allocate the Level objects to avoid per-update allocation.
The Update Processing Loop
A realistic update loop for incremental FIX market data:
| |
levelPool is a pre-allocated object pool. Level objects are recycled rather than GC’d. At 50k updates/second this keeps allocation rate near zero on the hot path.
updateBest only runs on NEW and DELETE actions — the best price only changes if a new top-of-book level appears or the current one disappears. On CHANGE (quantity update at existing level), the best price doesn’t change.
Snapshot vs Incremental State
Two book subscription models exist in FIX:
- Snapshot (MsgType W): full book state, all levels. Use to initialise or recover.
- Incremental (MsgType X): delta updates. Use for ongoing maintenance.
A common failure mode: applying an incremental update to a stale snapshot. If you missed some updates while reconnecting, your book is wrong. The correct recovery sequence:
1. Subscribe to incremental updates → buffer them (don't apply yet)
2. Request snapshot → apply to fresh empty book
3. Apply buffered incrementals that arrived after the snapshot's sequence
4. Switch to live incremental processing
Miss any of these steps and you’ll have a book that shows stale levels that no longer exist, or missing levels that do. Trading from a stale book is how fat-finger errors happen.
What We Actually Ran
In production: the array-based book for the 8 high-volume pairs (EUR/USD, GBP/USD, USD/JPY, USD/CHF, the majors), TreeMap-with-Long-keys for the 40+ less active pairs where update rates were low enough that allocation overhead didn’t matter.
The hybrid approach kept the code surface manageable — the TreeMap implementation was the reliable fallback and handled the long tail of instruments. The array implementation handled the instruments where performance mattered. Both exposed the same interface; the pricing engine didn’t know which it was talking to.