ProductionSimulation is a durable DMA harness with two accounting modes:
MATCHING_ONLYfor externally risk-managed order flow; andFULL_EQUITY_RISKfor fully funded share and cash accounting with an asynchronous Emporia portfolio-service boundary.
Every DMA operation is published through one serial lane selected by:
partition = symbol & (symbolPartitions - 1)
The partition count must be a power of two and equal the matching-engine count. Operations for one symbol are published FIFO while different partitions can publish concurrently. Results expose the partition and publication sequence.
A checkpoint first persists every native exchange shard. It then writes the DMA lifecycle to a versioned, CRC32C-protected temporary file, forces the contents, atomically renames it and forces the parent directory.
The lifecycle file is the checkpoint commit marker. Recovery loads it before starting exchange-core from the matching checkpoint ID. Missing, truncated or corrupt lifecycle data rejects recovery. Partial fills and completed delivery responses are restored, so idempotent duplicate delivery survives restart.
This is checkpoint recovery, not continuous journal replay. Operations after the latest committed checkpoint are outside its recovery boundary.
ProductionSimulation.metrics() provides cumulative lock-free counters for
submissions, completions, failures, duplicate deliveries, fills, and filled,
cancelled and rejected quantity. Portfolio publication failures are reported
separately while retaining the exchange fill or rejection outcome. Metrics
also include approximate p50, p95, p99 and maximum latency plus aggregate
successful operations per second.
Cached duplicate responses do not count their fills or quantities twice.
ProductionSimulationAccounting.fullEquityRisk(gateway) configures
FULL_PER_CURRENCY risk with margin disabled. In this mode:
onboardPortfolio(clientId)imports a one-timeEmporiaPortfolioSeed;- the core rejects an ASK without sufficient equity and a BID without sufficient cash;
- successful trades transfer equity and cash in-engine;
- every completed DMA delivery publishes the affected clients'
EmporiaPortfolioSnapshotobjects; and adjustPortfolioBalance(...)applies an idempotent funding transaction and republishes the resulting available balances.
The gateway remains transport-neutral. A concrete asynchronous HTTP
implementation is provided by HttpEmporiaPortfolioGateway. Published
snapshots must be deduplicated by (deliveryId, clientId). A failed
publication does not roll back an exchange command; redelivering the same DMA
request republishes the cached result and portfolio snapshots.
Portfolio snapshots contain available balances. Funds reserved by live orders remain represented by the order projection and are returned to available balances on cancellation.
Full-equity-risk mode accepts only EQUITY symbols. DMA limit, protected IOC,
and cancel operations are supported. Atomic replace remains matching-only
because the full risk engine does not yet maintain enough reservation metadata
to re-reserve an existing order atomically.
Native risk state and DMA lifecycle state use the same checkpoint. The accounting mode is stored in the lifecycle commit marker, so recovery rejects a mode mismatch.
The adapter calls these endpoints relative to its configured base URI:
GET internal/v1/portfolios/{clientId}/risk-seed
PUT internal/v1/portfolio-snapshots/{deliveryId}/{clientId}
The seed response uses a balance array so numeric asset identifiers do not become JSON object keys:
{
"schemaVersion": 1,
"clientId": 101,
"firstTransactionId": 7001,
"balances": [
{"assetId": 840, "amount": 50000},
{"assetId": 20001, "amount": 12}
]
}Snapshot requests are deterministic and sort balances by asset ID:
{
"schemaVersion": 1,
"exchangeId": "emporia-simulation-1",
"deliveryId": 13,
"clientId": 101,
"availableBalances": [
{"assetId": 840, "amount": 0},
{"assetId": 20001, "amount": 5}
]
}Every snapshot request includes:
Content-Type: application/json
Accept: application/json
Idempotency-Key: {exchangeId}:{deliveryId}:{clientId}
Emporia must return any 2xx response for a new snapshot or an identical
duplicate. It must reject reuse of the idempotency key with a different body.
The adapter marks HTTP 408, 429, and 5xx responses, plus transport
failures, as retryable through EmporiaHttpException.retryable(). It does not
retry internally.
Configure and wire the adapter with credentials supplied by the runtime:
var gatewayConfiguration = new EmporiaHttpGatewayConfiguration(
URI.create(System.getenv("EMPORIA_PORTFOLIO_BASE_URL")),
"emporia-simulation-1",
Duration.ofSeconds(3),
Map.of(
"Authorization",
"Bearer " + System.getenv("EMPORIA_PORTFOLIO_TOKEN")));
var gateway = new HttpEmporiaPortfolioGateway(gatewayConfiguration);
var accounting =
ProductionSimulationAccounting.fullEquityRisk(gateway);
var simulation =
ProductionSimulation.start(configuration, accounting);The direct HTTP gateway remains useful for development: its
CompletableFuture completes when Emporia returns a successful response. Use
DurableEmporiaPortfolioGateway in a production simulation. Its future
completes after PostgreSQL commits the immutable outbound event, while a
separate lease-based worker delivers that event to Emporia.
Apply
src/main/resources/db/portfolio-outbox/V1__create_portfolio_outbox.sql to a
PostgreSQL database owned by the embedding exchange process. Then supply a
managed JDBC DataSource:
var http = new HttpEmporiaPortfolioGateway(gatewayConfiguration);
var outbox = DurableEmporiaPortfolioGateway.start(
http,
dataSource,
PortfolioOutboxConfiguration.defaults(instanceId));
var accounting =
ProductionSimulationAccounting.fullEquityRisk(outbox);
try (outbox;
var simulation =
ProductionSimulation.start(configuration, accounting)) {
// Submit DMA operations.
}The outbox stores the exact encoded request bytes and SHA-256 digest. Enqueuing
the same event and content is idempotent; reusing an event ID with different
content fails. Workers atomically claim ready rows with a lease, deliver
different clients concurrently, and preserve sequence order for one client.
An expired IN_FLIGHT lease is eligible for another worker after a process
crash.
Transport failures, HTTP 408, 429, and 5xx responses move a row to
RETRY with capped exponential jitter. Other HTTP 4xx responses move it to
DEAD. Because events are full snapshots, a dead row does not block a later
snapshot forever; operators should still alert and investigate it. A successful
or identical duplicate response moves the row to PUBLISHED.
Published and dead rows are retained for audit and idempotency rather than deleted automatically. Define an operator-owned retention or archival job before sustained load; never purge pending, retrying, or leased rows.
Useful backlog checks include:
SELECT status, count(*), min(created_at)
FROM exchange_core_portfolio_outbox
GROUP BY status;
SELECT event_id, client_id, attempt_count, next_attempt_at, last_error
FROM exchange_core_portfolio_outbox
WHERE status IN ('RETRY', 'DEAD')
ORDER BY sequence_id;Run the real PostgreSQL outbox specification with:
mvn -Ppostgres-it testThis verifies duplicate enqueue, per-client ordering, and reclaim after a publisher process loses its lease.
The outbox guarantees delivery survival once publish() commits the row. It
does not make the earlier exchange mutation and outbox insert one database
transaction. ProductionSimulation currently has checkpoint recovery with
continuous journaling disabled, so a crash after an exchange command but
before enqueue remains a gap.
Closing that final gap requires continuously journaling the DMA inbox and lifecycle and deterministically replaying every operation after the last checkpoint, including regeneration of the same portfolio event ID and payload. Do not claim strict end-to-end crash recovery until that replay boundary is implemented and tested.
The standalone benchmarks/ JMH project measures
partition dispatch, matching-only and full-risk protected IOC round trips,
portfolio publication, and durable checkpoint latency.