Java 21 · Lock-free · Object-pooled hot path

A low-latency
order matching engine,
built from first principles.

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.

0M orders / sec, realistic mixed workload
<0µs p99 latency
0 GC events, 10s steady-state window
0 property-test sequences

Interactive

The order book, live

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.

orders 0 fills 0 spread -
Depth ladder AAPL · tick = $0.01
Order ticket
Trade tape maker's price

In-browser simulation for illustration. The real engine is single-threaded Java behind a Disruptor ring buffer.

Numbers

Benchmarks measured

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.

- orders matched / sec, realistic mixed workload
- GC events in 10s steady-state window
- bytes allocated / op, hot path
- randomized sequences, jqwik property tests
End-to-end latency distribution -

Design

One thread. No locks. Less garbage.

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.

01

Order intake

Producers publish into pre-allocated OrderEvent slots. Ring size is a power of two, so modulo is a single bitwise AND.

Phase 5 · LMAX Disruptor
02

Matching core

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 + pool
03

Order book

Two TreeMaps (bids descending, asks ascending) so firstKey() is always the best price. ArrayDeque FIFO per level enforces time priority.

Phase 1 · Book structure
04

Fills out

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.

Phases 8–9 · Codec + tape

Why single-threaded?

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.

Why pool objects?

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.

Why fixed-point prices?

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

The details that matter at microsecond scale

Price-time priority

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.

Maker-price fills

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.

Typed order semantics

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.

Property-based proof

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.

Single-writer ring buffer

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.

Cache-line padding

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.

Off-heap serialization

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.

Honest measurement

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.