Four views of the same platform: the layers it is built in, the paths a request takes
through it, where the trust boundaries sit, and order placement from the click to the blotter.
The block diagram answers "what is connected to what". This page answers "why is it shaped that
way". Three constraints drive nearly every decision here. Execution is asynchronous and can fail, so
the intent to trade is recorded before the trade happens. The record is the position, so it survives a
restart and a bad deployment. And the question "what is my balance now" has nothing in common with
"what did the desk trade last quarter", so the two are answered by different stores.
Layered view
Each layer depends only on the layers below it. The sprint number tells you when the layer is built,
which is also the order in which the dependencies become real.
The domain package and the extension modules are inside the Trade REST API, and the
market-data poller is inside the Trade Executor. Neither is a separate deployable.
The write path and the read path
These two paths have different shapes on purpose. The write path crosses a broker and finishes some
time after the HTTP response has already been sent. The read path is one request, one query, one
answer. Confusing them is the most common Sprint 7 mistake: an order ticket that waits for
FILLED in the response will wait forever.
Trust boundaries
Three zones, and each one trusts less than the one below it. The rule that matters most is the one
that looks like a detail: the Trade REST API verifies the JWT signature itself rather than trusting an
upstream to have done it. A service that trusts a header it did not verify is a service that can be
talked into anything.
The crossed connection is drawn on purpose. Every cohort has someone who calls the
Fauxnance API from Angular because it is quicker, and the key ends up in a bundle on a CDN.
Order placement, end to end
Every participant must be able to describe this path from memory by the end of Sprint 7. The domain
call and the token check are both in-process, which is why they appear as messages a participant sends
to itself rather than as separate lifelines.
Dashed lines are returns. The gap between step 9, where the API answers, and step 15, where
the outcome is published, is the whole point of Sprint 7.
Points that are commonly got wrong
The HTTP response returns before the fill happens. From Sprint 7 onwards POST
/api/v1/orders returns status: NEW. In Sprint 6, before the executor exists, the
same endpoint fills in process and returns FILLED or REJECTED. The contract
permits both, and the frontend has to handle both.
The cash debit and the position update belong to the executor, in one transaction. The API only
validates and records intent.
Idempotency is enforced by a unique constraint on orders.idempotency_key, not by an
application-level check. A duplicate key is a 409 ORD-409.
The executor is a consumer group. Two instances must not double-fill one order. Keying by
accountId and guarding the status transition inside the transaction is what makes that
safe.
Rejections are events too. A rejected order publishes to trade-events with
eventType: ORDER_REJECTED, so notifications and analytics see it. A consumer that only
ever sees fills reports a fill rate of 100 per cent.
The domain call is now in process, which does not make the layering optional. No SQL in a
controller, no HTTP type in the domain package, and no Spring annotation in the domain package beyond
validation.
Operational and analytical split
Two stores, two models, two access patterns. After Sprint 7 the dashboard never points at the
operational database again.
Concern
Operational
Analytical
Store
PostgreSQL 16
DuckDB, one file on disk
Model
Normalised to third normal form, per contracts/database-schema.sql
Star schema, per contracts/analytics-schema.sql
Written by
Trade REST API, Trade Executor, auth service
The analytics ETL only
Read by
All services, and the frontend through their APIs
The dashboard, notebooks, extension analytics
Latency
Milliseconds, single row
Seconds, full scan and aggregate
Retention
Current state plus full order history
Append-only history, dimensions with effective dates
Failure impact
Trading stops
Reporting is stale
Sprint 4 reads PostgreSQL directly because the analytical store does not exist yet. Sprint 7 moves
it. Batch extract is the source of truth for fact_trades; consuming
trade-events is optional and, where a team builds it, must reconcile against the batch
load rather than replace it.
Trust boundaries in full
Boundary
Control
Sprint
Browser to any service
HTTPS in deployed environments, TLS 1.2 minimum
11
Frontend to auth service
Credentials over HTTPS only, argon2 or bcrypt at rest, no password ever logged
8
Frontend to Trade REST API
Signed JWT in the Authorization: Bearer header, verified on every /api/** route
6 with the stub, 8 with the real service
Route to route inside the Trade REST API
Every route authorises the account itself. A service boundary is no longer doing it for you, so an extension route that returns another customer's portfolio is a bug inside the application that holds the order book.
10
Trade REST API to PostgreSQL
Parameterised statements through MyBatis, a least-privilege application role, no DDL rights
3 and 6
Any service to Kafka
Plaintext locally. Document the TLS, SASL and ACL configuration you would apply in production, and do not claim it is implemented.
7
Any service to the Fauxnance API
X-Api-Key from an environment variable. Never commit a key. Never send a key to the browser.
7
Deployed frontend to S3
Private bucket, origin access control, reachable only through CloudFront