Research: YU10-fix-ingress
Why FIX, and why nowβ
The measured ingress tiers on this system are REST single-order ~9.2k/s, REST /orders/batch
~74k/s, and the in-process journaled BLP at ~1.26β1.59M/s (scripts/bench/README.md). Two
ingress-internal experiments (async MVC, gateway microbatching) both REGRESSED single-order
throughput; the ceiling is the per-order HTTP envelope and blocked round-trip, not the engine.
FIX's session model is the standard order-entry answer: orders stream in on a long-lived session,
ExecutionReports stream out asynchronously, and no per-order round-trip exists to block on. It is
also the protocol integration surface an order-management system is measured by.
Engine selection: QuickFIX/Jβ
The FIX session layer; logon, heartbeats, test requests, sequence management, resend windows, gap fills, duplicate flags, persistent stores; is the part that is counterparty-visible and unforgiving. QuickFIX/J implements all of it, is mature and widely deployed, and its configuration surface (session settings, store implementations, threading models) covers this state's needs without custom transport code.
Alternatives considered:
- artio (Aeron-based FIX engine): allocation-free session handling on the ingress threads. Rejected here because the ingress threads sit outside this system's measured no-GC boundary (the allocation gates cover producer/journaler/BLP threads; the REST edge already allocates per request and the gates pass), so artio's core advantage is spent where it isn't measured, while its cost; an Aeron media driver as a hard dependency of the front door and a much smaller configuration/documentation surface; is paid immediately. The acceptor, translator, and report handler are engine-agnostic seams; adr-034 records the boundary.
- Hand-rolled FIX subset: writing FIX frames is simple; the resend/recovery machinery is not, and it is exactly what a counterparty exercises during incident recovery. Hand-rolling is used only where it is safe: the throughput load generator, which only sends.
Topology: in-process acceptorβ
A separate FIX gateway process must forward orders to the matcher over some internal transport,
re-introducing a per-order envelope and adding a second stateful process to recovery. In-process,
the session thread publishes translated events directly to the input ring
(ProducerType.MULTI; REST/Tomcat threads and the replication follower already share it), so a
FIX order is bit-identical to a REST order past the front door: same YU03 risk screen, same
journal, same replay. The trade-off accepted: a JVM-wide failure takes both front doors down
together; which is already true of REST, and is what the journal/recovery machinery exists for.
The state deploys in single-BLP mode and the FIX endpoint addresses the single replica directly;
session state does not follow BLP role movement (TD-FIX02).
Correlation: the ledger and inputSeqβ
OutputEvent carries inputSeq, correlating every lifecycle output back to the input-ring
sequence of the command that caused it; the same mechanism the REST gateway uses for its
acknowledgements. FIX correlation therefore needs no new field on pooled events, no snapshot or
wire-format change: the FIX layer records (session, ClOrdID, inputSeq, orderRef) durably BEFORE
publishing, and the report handler joins output events to sessions through that ledger. The
ledger doubles as the duplicate-ClOrdID detector and survives restarts by rehydration; adr-035
records why the alternative designs (a session/origin field on OutputEvent; journaling the FIX
identity in the input record) were rejected.
Identity: fail-closed at logonβ
EntitlementGate.resolve validates a JWT Authorization value and returns a ResolvedPrincipal
that is resolved once and reused; the exact pattern the REST batch path uses per request. FIX
Logon carries the JWT in Password(554), so the existing gate API is used unchanged, once per
session. CompIDβaccount mapping is committed configuration; there is no fallback account. The
REST risk.entitlement.enforced flag (default false) governs the REST rollout only: a brand-new
ingress surface has no installed base to keep compatible, so it starts fail-closed. The endpoint
itself is cluster-internal (NodePort on kind for test clients); the deployed session profile is
plaintext TCP inside the cluster boundary.
Ambiguous outcomesβ
The REST gateway's acknowledgement wait demonstrates the problem: once a command is published to
the ring, a timeout no longer proves rejection; the journaler and BLP may still process it.
FIX makes the resolution natural: the eventual ExecutionReport is the outcome, OrderStatusRequest
is the standard pull-based reconciliation, and duplicate-ClOrdID detection makes a client retry
safe and deterministic. The four-outcome model (FR-FIX12) never converts an ambiguous state into
a definitive reject; adr-037 records it.
Store durabilityβ
QuickFIX/J's file store persists sequence numbers and sent messages; PersistMessages enables
resend-window recovery of ExecutionReports. The store's sync cadence bounds a small crash window
(TD-FIX01) whose compensating control is OrderStatusRequest; the journal remains the
authoritative fill record, and the YU05 regulatory export reconstructs fills independently of any
session store.