Java 21 · Lock-free · Object-pooled hot path
Price-time priority limit order book with LMAX Disruptor intake, object-pooled order matching, cache-line-padded sequences, and off-heap serialization. Every line typed by hand as a deep dive into JVM internals.
Interactive
This demo runs the same algorithm the Java engine implements: price-time priority, fills at the maker's price, FIFO within each level, mirrored in JavaScript. Submit your own orders or let the simulated flow run.
In-browser simulation for illustration. The real engine is single-threaded Java behind a Disruptor ring buffer.
Numbers
Measured with JMH + HdrHistogram on real hardware. Full methodology, GC-profiler breakdown, and a genuine benchmark-construction bug found and fixed along the way, in the benchmark report.
Design
The entire matching path runs on a single pinned thread fed by a lock-free ring buffer. Concurrency is handled by design (the single-writer principle), not by locking.
Producers publish into pre-allocated OrderEvent slots. Ring size is a power of two, so modulo is a single bitwise AND.
One pinned thread walks price levels, fills at the maker's price, purges empty levels. Orders come from a fixed pool, reused rather than freshly allocated.
Phases 2–3, 6 · Engine + poolTwo TreeMaps (bids descending, asks ascending) so firstKey() is always the best price. ArrayDeque FIFO per level enforces time priority.
Immutable Fill records returned inside MatchResult. A separate off-heap MemorySegment codec (42 bytes fixed-width, zero heap allocation) exists and is benchmarked standalone, not yet wired into this live path.
A matching engine must be deterministic: the same order sequence must always produce the same fills. One writer per book eliminates locks, CAS loops, and cache-line ping-pong. LMAX processed 6M TPS this way.
p99.9 latency spikes are usually GC-related. Escape analysis can't save you when orders escape into the book, so pooling (borrow, reset, return) cuts allocation on the hottest path. It doesn't reach zero: Fill records, MatchResult variants, and per-sweep lists still allocate, measured directly rather than assumed.
IEEE 754 rounding means 0.1 + 0.2 ≠ 0.3: in a matching engine that's a missed fill. Prices are long integer ticks everywhere. Floating point is banned from the codebase.
Engineering
Sorted TreeMap gives price priority; per-level ArrayDeque FIFO gives time priority. Best price is O(1) via firstKey(), cancel is O(1) via a HashMap side-table.
Fills execute at the resting order's price: the maker set the terms, the taker agreed. Locked in by a regression test that fails if anyone ever fills at the taker's price.
LIMIT, MARKET, IOC, FOK, each with typed results. FOK pre-checks liquidity before touching the book: fill 100% or reject with zero fills. No silent quantity leaks, ever.
jqwik generates 4,000 adversarial order sequences per test run (4 properties x 1,000 tries each) and asserts invariants: spread never negative, quantity never leaks, cancelled orders never linger. Shrinking reduces failures to minimal reproducers.
The Disruptor's secret: only one thread ever writes a slot. No CAS, no locks, sequential memory access the hardware prefetcher loves. YieldingWaitStrategy trades CPU for jitter-free latency.
Producer and consumer sequences live 64 bytes apart (7 longs of padding each) so a write on one core never invalidates the other's L1 line. False sharing measured, then eliminated.
Fixed-width 42-byte order layout in native memory via Panama's MemorySegment. Any field seekable in O(1). Zero heap allocation per encode/decode, verified with ThreadMXBean.getThreadAllocatedBytes.
JMH with 2 forks and 10-second warmup/measurement iterations, HdrHistogram via a double-buffered Recorder for tail latency. CI blocks any PR that regresses throughput more than 10% against a pinned baseline, wired into .github/workflows/bench.yml, not aspirational.