Data Model: LMAX Sequencer Architecture (Trading Hot Path)
Scopeβ
This state defines data model impact relative to 009-order-management-matcher. The external/relational
surface is preserved; the authoritative store moves from the database to the input journal, and a set of
in-memory and binary structures is introduced on the hot path.
Entity Changesβ
- Added: Input event (ring slot / SBE message, one mutable holder per slot, reused forever):
seq(long, global sequence number, strictly monotonic)type(byte:ORDER_NEW | ORDER_CANCEL | FORCE_FILL | PRICE_TICK | TRADE_NEW)accountId(int)securityId(int, mapped from ticker at the Gateway)side(byte: 0=Buy, 1=Sell)qty(long)limitPx(long fixed-point, Γ1,000,000)priceTicks(long fixed-point, forPRICE_TICK)ingressNanos(long, stamped at the Gateway; the only time source on the hot path)
- Added: Output event (output ring slot):
seq(long, echoes the producing input sequence)kind(byte:ORDER_ACCEPTED | ORDER_REJECTED | ORDER_PARTIALLY_FILLED | ORDER_FILLED | ORDER_CANCELED | TRADE_BOOKED | POSITION_UPDATED)accountId(int),securityId(int),side(byte)qty(long),pxTicks(long fixed-point),remainingQty(long)status(byte:NEW | PARTIALLY_FILLED | FILLED | CANCELED | REJECTED)ingressNanos(long, carried through for true end-to-end latency at egress)
- Added: Journal record (append-only, authoritative;
input-events.journal): fixed 64-byte little-endian record;seq, type, side, orderRef, accountId, securityId, qty, limitPx, priceTicks, eventTimeMillis(+ pad).ingressNanosis a latency field, not state, so it is not journaled. A torn trailing record (crash mid-append, < 64 bytes) is discarded on replay. (SBE-encoded bytes +schemaId/versionare the deferred perf-profile form; today the record is typed fields.) - Added: Snapshot (
snapshot.dat): full BLP state; orders, net positions, last prices,nextOrderRef,tradeCounter; pluscoveredOffset, the journal byte offset just past the most recentSNAPSHOTmarker. Written atomically (temp + atomic rename) on the BLP thread at a sequenced SNAPSHOT marker, on thesnapshot.interval.mscadence. Recovery loads it and replays only the journal tail aftercoveredOffset. Binary layout:magic|version|coveredOffset|nextOrderRef|tradeCounter, then count-prefixed prices, positions, and orders. - Added: Projection checkpoint: the projector's last projected
seqwatermark (projectedSeq), advanced only on a committed DB flush so re-projection is idempotent. In-memory in YU01 (no standalone checkpoint file yet); the snapshot'scoveredOffsetis the durable recovery boundary. - Added: Symbol table:
ticker (string) <-> securityId (int)mapping owned by the Gateway; strings never cross into the rings or BLP. Persisted assymbols.tabinjournal.pathand restored FIRST at boot, so security ids replayed from the journal resolve to the same tickers. - Added: in-memory BLP state (pre-allocated/pooled, never
new-ed mid-life):OrderBook[] booksBySecurity; array indexed bysecurityId; pooled resting-order entries, returned to the free list at terminal status.Long2ObjectHashMap<Position>keyed byaccountId Γ securityId(qty,avgPxfixed-point).long[] lastPxBySecurity; last price per security, fixed-point.Int2ObjectHashMap<Account>andInt2ObjectHashMap<Security>validation caches, event-fed, warmed at startup; misses resolved via request/response events.
- Changed:
OrderBooktable is demoted to an async read-model. Column shape from009(orderId,accountId,security,side,quantity,remainingQuantity,limitPricedecimal(18,3),status,createdAt,updatedAt,lastExecutionPrice,lastFillQuantity) is unchanged; rows are written by the batched Projector from output events instead of inline JPA. Trade/position rows likewise remain schema-identical and become projector-written on this path. - Changed: order IDs derive deterministically from the global sequence (the
009String.format("ord-013-%04d", β¦)scheme is replaced by a pure function ofseq, rendered to the existing external string shape at the output edge). - Removed: matcher-internal
ConcurrentHashMap<String,BigDecimal> lastPrices,AtomicIntegerorder-sequence counters, and per-tick JPA query results as state carriers (replaced by the structures above).
Compatibility Notesβ
- Backward compatibility requirements are reflected in
requirements/functional-delta.md,requirements/nonfunctional-delta.md, andcontracts/contract-delta.md. - Source of truth: the journal is authoritative; the relational read-model is rebuildable by
re-projection (FR-09B23). The
OrderBookschema generation contract from009(NFR-01312/NFR-01313) remains binding. - Open order semantics carry over from
009:open= status inNEW|PARTIALLY_FILLED;unfilled=remainingQuantity > 0. - Auto-fill policy semantics carry over from
009and are evaluated in fixed-point integer math:Buyin-the-money whenmarketPrice <= limitPrice;SellwhenmarketPrice >= limitPrice.- remaining
< 1000: full fill; otherwise half fill (rounded up), per triggering event.
- Fixed-point: global scale Γ1,000,000 (6 dp),
187.250 -> 187_250_000L. AllBigDecimal/string rendering happens at the Gateway/output edges; rounding is locked to009behavior by the penny-parity fixture (SC-09B04). - Eventual consistency (by design): UI is push-fed from output events; the relational read-model is a slightly-behind projection. REST bootstrap reads remain correct because the projector checkpoint is monotonic over the same ordered stream.
Ring sizingβ
slots_needed >= peak_input_rate Γ max_handler_stall Γ safety_factor, rounded up to a power of two.
Demo example: 50,000 events/s Γ 2 ms journaler stall Γ 4 = 400 β generously rounded to
2^16 = 65,536 (input and output). Perf profile: 2^20. Memory β ring_size Γ (holder + off-heap slot buffer) (~256 MB at 2^20 Γ 256 B), pre-touched at startup. Config keys:
disruptor.input.ring-size, disruptor.output.ring-size.
Traceabilityβ
- Input stream/journal/snapshot shapes link to FR-09B01..FR-09B08, FR-09B16 in
spec.md. - BLP in-memory structures link to FR-09B10..FR-09B15 and NGC-01/NGC-04.
- Output event/read-model/checkpoint shapes link to FR-09B20..FR-09B25.
- Fixed-point and symbol-table rules link to FR-09B05, NGC-03, SC-09B04.