Enterprise Trading Platform · architecture diagrams

Trade Executor

Java 21, a Kafka consumer and a Kafka producer, health endpoint on 8082. The service that turns an accepted order into a fill or a rejection, and the service that makes a price stream exist.

Two jobs live here and they share nothing except a container and a Fauxnance key. The consumer reads orders, prices the order against a live quote, decides fill or reject, writes the order status, the cash movement and the position in one transaction, and publishes the outcome. The poller runs on its own schedule, calls the Fauxnance batch quotes endpoint for held and watched symbols, and publishes one message per symbol to market-data.

Putting them together answers a question every cohort asks: which service holds the API key. One service calls the Fauxnance API, so one service holds the key. It also keeps Python to the analytics estate and nowhere else.

Internal structure of the Trade Executor The orders topic and the internal scheduler on the left. Seven packages in the centre: messaging, execution, quote, marketdata, persistence, domain and config. On the right, the trade-events topic with its dead-letter topic, the Fauxnance API, the market-data topic and PostgreSQL, each aligned with the package that talks to it. INBOUND OUTBOUND Kafka, group trade-executor orders 3 partitions · key accountId auto-offset-reset earliest the service's own clock scheduled poll POLL_INTERVAL_SECONDS There is no inbound API. Nothing in the platform calls this service. It is reachable only through the broker and its own scheduler, which is why the only port it opens is a health check. Trade Executor :8082 health only Java 21 · Spring for Apache Kafka · JDBC · Spring Boot Actuator com.neueda.trading.executor messaging Consume, deserialise, acknowledge. OrderPlacedListener · OrderPlacedEnvelope · OrderPlacedPayload · NonRetryableMessageException TradeEventPublisher · TradeEventEnvelope · TradeEventPayload auto-commit disabled · ack-mode manual_immediate · concurrency 3 · dead-letter suffix .DLT execution Decides fill or reject. No I/O of its own. OrderExecutionService · FillPolicy · ExecutionLatency rejects with INSUFFICIENT_FUNDS, PRICE_NOT_MET, INSTRUMENT_NOT_TRADABLE quote The one place a Fauxnance call is made. QuoteClient · FauxnanceQuoteClient · CachingQuoteClient · Quote quote-max-age 5s · 3 attempts · 200ms backoff · connect 2s, read 3s marketdata Runs on a schedule, not on the order path. MarketDataPoller polls at a fixed interval for the symbols the platform holds and watches calls GET /quotes?symbols= with at most 25 symbols per call, because a batch call costs one unit RequestBudget guards the 2000 request daily quota and stops before the key is refused publishes one message per symbol, keyed by symbol, never one message per batch Written in Java in Sprint 7, in this service, beside the consumer it sits next to. persistence One transaction, three writes. ExecutionRepository · JdbcExecutionRepository · OptimisticLockConflictException rows: OrderRow · AccountRow · PositionRow guarded transition: UPDATE orders ... WHERE id = ? AND status = 'NEW'. Zero rows means already done. domain Its own small vocabulary, not the API's. OrderStatus · Side · RejectReason config Read once, at start-up. ExecutorProperties · FauxnanceProperties · KafkaConsumerConfig · ExecutionConfig topics · retry budget · latency window · optimistic-lock attempts and backoff trade-events key accountId · 30 days orders.DLT Fauxnance API GET /quotes/{symbol} GET /quotes?symbols= X-Api-Key, from the environment market-data key symbol · 6 partitions source is market-poller PostgreSQL the order status transition, the cash movement and the position, in one transaction ENVIRONMENT DB_URL · DB_USERNAME · DB_PASSWORD · DB_POOL_SIZE · KAFKA_BOOTSTRAP_SERVERS · ORDERS_TOPIC · TRADE_EVENTS_TOPIC · MAX_DELIVERY_ATTEMPTS EXECUTION_LATENCY_MIN · EXECUTION_LATENCY_MAX · FAUXNANCE_BASE_URL · FAUXNANCE_API_KEY · QUOTE_MAX_AGE · POLL_INTERVAL_SECONDS · MARKET_DATA_SYMBOLS
Each outbound box is drawn level with the package that talks to it, so no line crosses another. The poller is a package inside this service, not a container beside it.

Why the settlement is one transaction

A fill changes three things: the order status, the cash balance and the position. If those are three separate commits, a crash between them leaves an account that paid for shares it does not hold, or holds shares it did not pay for. Neither is recoverable from the outside, because nothing records that the sequence was half done.

The guarded transition is what makes a replayed message safe. Update the order WHERE id = ? AND status = 'NEW', and treat zero rows affected as work somebody else has already done. No extra table, and it holds under concurrency because the database serialises the update. This is the Sprint 7 acceptance check: replay a consumed ORDER_PLACED message and show that the cash balance does not move twice.

The two loops, side by side

Execution loopPoller loop
Triggered byA message on ordersThe scheduler, every POLL_INTERVAL_SECONDS
Fauxnance callGET /quotes/{symbol}, one symbolGET /quotes?symbols=, up to 25 symbols
DatabaseReads and writes, in one transactionNone at all
PublishesOne message to trade-events per orderOne message to market-data per symbol
Failure modeRetry with backoff, then the dead-letter topicSkip the cycle, log it, and try again on the next tick
Gets it wrong whenThe offset is committed before the work is doneThe interval is short enough to exhaust the daily quota before lunch

Delivery semantics you have to configure

The platform runs at-least-once. Producers retry, consumers commit after processing, and duplicates therefore happen. Plan for them rather than trying to eliminate them.