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.
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 loop
Poller loop
Triggered by
A message on orders
The scheduler, every POLL_INTERVAL_SECONDS
Fauxnance call
GET /quotes/{symbol}, one symbol
GET /quotes?symbols=, up to 25 symbols
Database
Reads and writes, in one transaction
None at all
Publishes
One message to trade-events per order
One message to market-data per symbol
Failure mode
Retry with backoff, then the dead-letter topic
Skip the cycle, log it, and try again on the next tick
Gets it wrong when
The offset is committed before the work is done
The 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.
Producer: acks=all, enable.idempotence=true, retries high, in-flight
requests capped at five so ordering survives a retry.
Consumer: auto-commit off, an explicit group.id, and the offset committed after the
database commit and the publish.
Dead letters: a malformed message goes to orders.DLT on the first attempt, because it
will never succeed. A transient failure, for example a 503 from Fauxnance, is retried with backoff and
only dead-lettered when the retry budget is spent.
Ordering is guaranteed inside a partition and nowhere else. That gives you order per account and
per symbol, which is all the platform ever promised.