Feature Specification: LMAX Sequencer Architecture (Trading Hot Path)
Feature Branch: YU01-lmax-sequencer
Created: 2026-06-09
Status: Draft
Input: Transition delta from 009-order-management-matcher, derived from the design proposals LMAX-SEQUENCER-ARCHITECTURE.md, LMAX-INPUT-DISRUPTOR.md, LMAX-BLP.md, LMAX-OUTPUT-DISRUPTOR.md, and LMAX-NO-GC-JAVA.md (repo root)
This state re-architects the trading hot path of state 009 around the LMAX architecture: one
globally sequenced, journaled, replicated input stream feeding a single-threaded,
in-memory Business Logic Processor (BLP), wired with LMAX Disruptor ring buffers and engineered for
zero steady-state allocation (no-GC). Every external contract from 009 (REST/WS endpoints, NATS
subjects, payload shapes, UI behavior, OrderBook schema) is preserved verbatim; only the execution
model changes. account-service, position-service, people-service, reference-data, the Angular
UI, ingress, and the LGTM observability stack remain on the current stack.
Requirement IDs use the 09B block (FR-09Bxx, NFR-09Bxx, SC-09Bxx). Cross-cutting no-GC
conformance requirements use the NGC namespace defined in requirements/no-gc-conformance.md.
User Storiesβ
- As a trader, I want order submit/cancel and matcher fills acknowledged with deterministic sub-millisecond in-node latency, so the order workflow remains instantaneous under load instead of waiting up to a full matcher polling tick.
- As a trader, I want every existing view (trade blotter, position blotter, account order blotter, admin order blotter, order ticket) to keep working with zero front-end change, so the re-architecture is invisible at the UI contract.
- As a platform engineer, I want every state-mutating input (orders, cancels, force-fills, price ticks, market trades) on one totally-ordered, journaled stream, so any production incident can be replayed deterministically in a dev environment.
- As an operations user, I want snapshot + journal-replay recovery and warm-standby failover, so a matcher restart or node loss does not lose or reorder accepted orders.
- As a maintainer, I want the no-GC allocation contract enforced by automated CI gates (Epsilon GC, static banned-API checks, penny-parity fixtures), so the latency properties are proven rather than assumed.
- As a maintainer, I want this transition to remain spec-first, with the sequencer/ring/BLP/read-model contracts documented before code generation.
Functional Requirementsβ
Sequenced input stream (Sequencer + Input Disruptor)β
- FR-09B01: All state-mutating inputs (order create, cancel, force-fill, price ticks, and market trades) SHALL enter through a single Gateway, be assigned a strictly monotonic global sequence number, and be written into a single pre-allocated input ring buffer (power-of-two capacity).
- FR-09B02: The matcher SHALL be event-driven. The
@Scheduledpolling tick from009(order.matcher.tick-ms) SHALL be removed; matching reacts to event arrival. - FR-09B03: Each input event SHALL be processed by three parallel input handlers; Journaler (durable
append), Replicator (stream to replicas), Un-marshaller (decode); with the BLP gated behind a
sequence barrier at
min(journaler, replicator, unmarshaller), so every event the BLP acts on is already durable, replicated, and decoded. - FR-09B04: Each input event SHALL be journaled before the BLP processes it; the journal is the authoritative system of record for trading state.
- FR-09B05: Prices and quantities SHALL be carried as
longfixed-point (global scale Γ1,000,000) and securities asint securityIdon the hot path;BigDecimal/Stringconversions SHALL occur only at the edges (Gateway in, output handlers out). - FR-09B06: Price ticks SHALL enter the sequenced input stream as
PRICE_TICKevents through the Gateway rather than out-of-band NATS subscriptions inside the matcher; orders and prices form one totally-ordered stream. - FR-09B07: Ring-full conditions SHALL apply bounded backpressure at the producer claim (no unbounded queuing); remaining capacity SHALL be observable as a metric.
- FR-09B08: Market trades from the trade ticket SHALL enter the sequenced stream as
TRADE_NEWinput events via the Gateway, so trade booking and position keeping share the single-writer path (the inline booking role oftrade-processoron this path is fused into the BLP). - FR-09B09:
trade-serviceSHALL remain the validating edge for market trades; validating the ticker (reference-data) and account (account-service) exactly as in009/008; and on success SHALL forward the validated trade to the order-matcher Gateway (which sequences it asTRADE_NEW) instead of publishing to the/tradesNATS subject. ThePOST /trade/request/response contract (HTTP 200 echoing the trade) is unchanged (FR-09B40) and the forward is fire-and-forget (booking is async on the BLP, matching009's publish semantics). The client does not supply a price; the execution price is stamped by the BLP (FR-09B17), subsuming008'strade-serviceprice-stamping role (FR-1002).
Business Logic Processor (BLP)β
- FR-09B10: Matching, trade booking, and position keeping SHALL execute on one thread, entirely in memory, within the handling of a single input event (no network hops or DB access between match, book, and position update).
- FR-09B11: The BLP SHALL make no blocking external calls (no REST, no JPA/DB, no NATS); anything not in memory SHALL be resolved via asynchronous request/response event pairs.
- FR-09B12: Validation that
009performed via blocking REST (account,stocks, latestprice) SHALL be served from in-memory caches kept fresh from the event streams and warmed at startup. - FR-09B13: The
009autofill policy and lifecycle SHALL be preserved exactly: in-the-money test (Buy: marketPrice <= limitPrice,Sell: marketPrice >= limitPrice), remaining< 1000fills fully, otherwise half (rounded up); statusesNEW | PARTIALLY_FILLED | FILLED | CANCELED | REJECTED. - FR-09B14: The BLP SHALL be deterministic: no wall-clock reads, no unordered-collection iteration, no
RNG/UUID on the hot path; timestamps are carried in events (
ingressNanosstamped at the Gateway) and order IDs derive from the global sequence. Trade IDs SHALL derive from a BLP-assigned monotonic trade number (trd-09b-<n>, noUUID/RNG), warm-seeded above the maximum persisted trade id at startup so ids are replay-stable and never collide across restarts (replacing009'sUUID.randomUUID()); both the projector and the NATS bridge derive the same id from the carried trade number. - FR-09B15: The BLP SHALL emit typed output events (
OrderAccepted,OrderRejected,OrderPartiallyFilled,OrderFilled,OrderCanceled,TradeBooked,PositionUpdated) into the output ring as its sole side effect channel, rather than POSTing trades or writing the DB inline.TradeBookedSHALL carry the stamped execution price (FR-09B17) andPositionUpdatedthe resulting net quantity and weighted average cost basis (FR-09B18); a fill SHALL emit its order update,TradeBooked, andPositionUpdatedas one paired ring claim. The009output-ring trade-submit handler that re-POSTed fills totrade-serviceSHALL be removed. - FR-09B16: BLP state SHALL be recoverable via periodic full-state snapshot (
snapshot.dat) plus journal-tail replay to the last journaled sequence, bounded by the snapshot interval. Recovery is selectable viarecovery.source:db(default) warm-starts the BLP from the persisted read-model and verifies that snapshot+tail replay reconstructs the same state, whilejournalrebuilds the live BLP from snapshot+tail with no database (paired withoutput.projector.db.enabled=false). Warm-start from the persisted read-model SHALL restore the in-memory order book, the net positions (quantity AND weighted average cost basis), and the trade-number counter, so the single-writer BLP resumes consistent with durable state. (A JIT warm-up replay before going live is a deferred target; seegeneration/implementation-status.md.) - FR-09B17: The BLP SHALL stamp every booked trade with an execution price: an order fill books at its
fill execution price (last market price, or the limit price on a force-fill before any tick), and a
TRADE_NEWmarket trade books at the security's last sequenced market price (PRICE_TICK), defaulting to0(rendered0.000, never null) when no tick has been seen. The price is carried onTradeBookedand rendered to theTRADES.pricecolumn and NATS payload at the edge, preserving the008trade-price contract (FR-1001/FR-1003). - FR-09B18: The BLP SHALL keep, per
(accountId, securityId), the net quantity AND the volume-weighted average cost basis, updated on every fill and market trade using008/009's running-average formula (newAvg = (oldAvg*oldQty + execPx*signedQty) / newQty, reset to0when the net position is flat), computed inlongfixed-point on the hot path. The result is carried onPositionUpdatedand rendered toPOSITIONS.quantity/POSITIONS.averageCostBasisat the edge, preserving the008position contract (FR-1004). The position store SHALL be an allocation-free primitive structure (noHashMap/autoboxing/BigDecimal), subject to the no-GC and banned-API gates (NFR-09B02, SC-09B13).
Output Disruptor and read-modelβ
- FR-09B20: All BLP results SHALL be published into a single-producer output ring
(
ProducerType.SINGLE) and fanned out by parallel Marshaller, NATS Publisher, and Read-model Projector handlers. - FR-09B21: The NATS Publisher SHALL reproduce the exact
009subjects and payload shapes;/orders,/accounts/{accountId}/orders,/trades,/accounts/{accountId}/trades,/accounts/{accountId}/positions; so all UI consumers work unchanged. A singleTradeBookedSHALL be published to BOTH/trades(global, formerly produced bytrade-service) and/accounts/{accountId}/trades(formerly produced bytrade-processor), and aPositionUpdatedto/accounts/{accountId}/positions. The publishedTradepayload SHALL include the executionpriceand stateSettled, and thePositionpayload SHALL includeaverageCostBasis, byte-compatible with the008/009shapes;securityId -> tickerand fixed-point -> 3dp decimal rendering happen here at the edge (FR-09B25). - FR-09B22: Database writes SHALL move to the async, batched Read-model Projector off the
acknowledgement path; the
OrderBooktable and trade/position rows become a read-model projected from output events, preserving the009schema contract. The order-matcher Projector SHALL be the SOLE writer of theOrderBook,TRADES, andPOSITIONStables (replacingtrade-processor's inline JPA on this path), writingTRADES.priceandPOSITIONS.averageCostBasisfrom the carried output events.trade-processorremains deployed for its REST read endpoints and smoke-suite health parity but books no trades and writes no positions on the order/market-trade path; the009fill -> trade-service -> /trades -> trade-processorbooking round-trip is removed (superseding FR-01310). - FR-09B23: The read-model SHALL be rebuildable by re-projecting the journal (recovery and schema
migration path), resuming idempotently from a persisted projection checkpoint (
last projected seq). - FR-09B24: A slow or unavailable read-model DB or NATS bus SHALL NOT block matching beyond the bounded output ring; the affected handler lags and catches up after recovery.
- FR-09B25:
securityId -> tickerandlong fixed-point -> decimalconversions SHALL occur in the output handlers (the edge), never in the BLP.
Replication and failoverβ
- FR-09B30: The replicated input stream SHALL support follower BLPs (second node in the primary site plus an optional DR site) that consume the identical sequenced stream in lock-step and suppress output until promoted.
- FR-09B31: On leader failure, a follower SHALL be promotable at its current sequence without cold replay (warm-standby failover); promotion SHALL NOT lose or reorder any journaled input.
- FR-09B32: The demo/
C2runtime profile MAY run a single replica with replication in loopback/stub mode; the replication contract SHALL still be exercised by tests in that profile.
Contract preservationβ
- FR-09B40: External contracts from
009SHALL be unchanged: order/trade/position REST and WS endpoints and response shapes, NATS subjects and payload contracts (including FR-01311/FR-01312 semantics), UI behavior (order ticket, blotters, admin view per FR-01303..FR-01305), and the API explorer / pub-sub inspector surface from FR-01315. - FR-09B41: Existing
009order metrics (traderx_orders_open_total,traderx_orders_unfilled_total,traderx_orders_pending_by_side,traderx_order_events_total,traderx_order_match_latency_seconds,traderx_order_book_age_seconds) SHALL be retained, now sourced from in-memory BLP state;traderx_order_match_latency_secondsbecomes a real measurement (no zero-filled placeholder). - FR-09B42: The inherited state-aware header contract (
009FR-01317) SHALL render this state's identity: generated snapshots update the state-ui-metadata overlay so the UI title, About page, and status view identifyYU01-lmax-sequencer(not the parent state id). This is the only permitted UI-visible change in this state.
Non-Functional Requirementsβ
- NFR-09B01: Latency budgets (performance profile, per
LMAX-SEQUENCER-ARCHITECTURE.mdΒ§11): BLP business logic p99< 25 Β΅s; output ring + marshal p99< 20 Β΅s; in-node compute (Gateway ingest to output emit, excluding network/durability acks) p99< 150 Β΅s; end-to-end including durable + replicated acknowledgement p99< 3 ms. Reported as full HdrHistogram distributions (p50/p99/p99.9/max), never means only. - NFR-09B02: Zero steady-state allocation on the hot path (Gateway encode, input ring, BLP, output ring
emit), per the no-GC conformance profile
NGC-01..NGC-08inrequirements/no-gc-conformance.md, enforced by an Epsilon-GC allocation gate in CI. - NFR-09B03: Determinism; an identical journal SHALL produce identical BLP state and identical emitted output events on replay.
- NFR-09B04: Single-writer discipline; no locks or atomics on the hot path; the BLP is the sole writer
of order books and positions;
orderMutationLockandAtomicInteger/AtomicLongcounters from009are removed. - NFR-09B05: Recovery; snapshot + journal replay restores matcher state to the last journaled sequence
with restart inside the target window (
< 1 minute), including JIT warm-up before going live. - NFR-09B06: Run profiles; a
demo/C2profile (BlockingWaitStrategy, no core pinning, no hugepages, single replica) that is the container default, and aperfprofile (BusySpinWaitStrategy on BLP/Journaler, pinned isolated cores, ZGC/Shenandoah, large pages, replicas + DR) documented for bare metal. Both profiles MUST pass the allocation gate; latency budgets in NFR-09B01 apply toperf. - NFR-09B07: Backpressure and throughput; sustained demo load runs with bounded ring backpressure and no GC pauses; ring remaining-capacity and sequence-lag gauges exported.
- NFR-09B08: Observability; all input/BLP/output/no-GC metrics in
requirements/nonfunctional-delta.mdare exported to Prometheus, scraped per the009mandatory scrape policy (NFR-01308), and represented in provisioned Grafana dashboards (ring headroom, sequence lag, journal/replication latency, BLP event latency, egress latency, projector lag, allocation rate with alert at> 0, GC pause panel). - NFR-09B09: Convergence level
C2is preserved:.github/workflows/build-and-publish.yml, image namespaceghcr.io/finos/traderx-c2/<component>with commit-SHA +latesttags, GHCR run bundle, and deployment bundle underruntime/deploy/per NFR-01309..NFR-01311 and NFR-01318..NFR-01319. - NFR-09B10: Inherited stacks remain intact:
007LGTM observability,008pricing/NATS contracts, and the ADR-013 push-over-polling realtime model (NFR-01314/NFR-01315) are unchanged. - NFR-09B11: The generated
database/initialSchema.sqlOrderBookcontract (NFR-01312/NFR-01313) remains satisfied; rows are now written by the Read-model Projector instead of inline JPA on the match path. - NFR-09B12: New dependencies (Disruptor, Agrona, SBE, Chronicle Queue / Aeron, OpenHFT Affinity, HdrHistogram, JMH) are pinned to latest CVE-clean releases and pass the repo dependency CVE gate.
Success Criteriaβ
- SC-09B01: Generation hook exists and is runnable
(
pipeline/generate-state-YU01-lmax-sequencer.sh). - SC-09B02: State smoke test path is defined
(
scripts/test-state-YU01-lmax-sequencer.sh). - SC-09B03: Functional parity with
009: order create, account-filtered listing, user cancel, admin force-fill, auto-fill policy, and resulting trade/position updates produce identical REST/WS responses and NATS events to009for the same scenarios. - SC-09B04: Penny parity;
longfixed-point fill arithmetic matches009'sBigDecimaloutcomes across a rounding fixture (no penny drift). - SC-09B05: No-GC gate;
pipeline/validate-no-gc-conformance.shruns the hot path under-XX:+UseEpsilonGCwith a small fixed heap and fails on any steady-state allocation; passes when allocation-free (SC-NGC-01). - SC-09B06: Determinism; a captured journal replayed in a clean process yields identical BLP state and identical emitted output events.
- SC-09B07: Latency; HdrHistogram reports meet NFR-09B01 budgets on the
perfprofile, with jHiccup confirming no GC-induced tail spikes. - SC-09B08: Recovery; snapshot + journal replay restores state to the last journaled sequence and the node restarts within the NFR-09B05 window.
- SC-09B09: UI parity; Angular blotters (trade, position, account orders, admin orders) update in real time via the unchanged subjects with no front-end change and no polling loops (re-validates SC-01311/SC-01313).
- SC-09B10: Decoupling; with the DB stopped, matching continues and the Projector catches up on recovery; with NATS stopped, matching continues and UI streams resume on reconnect.
- SC-09B11: Rebuild; dropping the read-model and re-projecting from the journal reproduces identical rows (idempotent from the projection checkpoint).
- SC-09B12: Failover; a follower BLP consuming the replicated stream is promoted after leader kill at
the same sequence with no journaled input lost (full check on
perfprofile; loopback/stub contract check ondemo). - SC-09B13: Banned-API gate; a static/architectural check asserts hot-path packages contain no
BigDecimal,Instant.now()/clock reads,HashMap/ConcurrentHashMap, stream pipelines,String.format, SLF4J parameterized logging,RestTemplate, or JPA references (SC-NGC-04). - SC-09B14:
C2; demo-profile images build, publish toghcr.io/finos/traderx-c2/<component>, and run correctly without core pinning, hugepages, or isolated cores, while still passing the allocation gate in CI (SC-NGC-06). - SC-09B15: Ack-path independence; measured order-acknowledgement latency is independent of DB and NATS latency (output fan-out and projection are off the acknowledgement path).
- SC-09B16: Generated snapshot branch and tag strategy are defined in the state catalog at
implementation time; generated branch artifacts include the
C2build/publish workflow and GHCR run-bundle assets. - SC-09B17: Trade & position field parity; for the same order-fill and market-trade scenarios, booked
trades carry the stamped execution
price(TRADES.price, finite on/tradesand/accounts/{accountId}/trades) and positions carry the volume-weightedaverageCostBasis(POSITIONS.averageCostBasis) identical to009/008'strade-processoroutputs, extending the penny-parity fixture (SC-09B04) to price and cost basis. - SC-09B18: Single-writer booking; submitting an order fill or a market trade books a trade and updates
the position written solely by the order-matcher Projector, with
trade-processorprocessing no trades on this path (no trade-booking activity) and positions not double-counted; the deterministic trade ids (trd-09b-<n>) remain stable and non-colliding across an order-matcher restart (warm-start fromPOSITIONSplus the seeded trade counter).