Enterprise Trading Platform · architecture diagrams

High-level design

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.

Layered view of the platform Seven layers from presentation down to the analytical store, each labelled with the sprint that builds it, with the Fauxnance API shown beside the execution layer as the only external dependency. Presentation Sprint 9 Frontend Angular · :4200 Login, dashboard, order ticket, blotter. Holds the JWT and attaches it through an interceptor. Guards every route except sign-in. Talks to two services and to nothing else. Security Sprint 8 Auth service NestJS · :3000 Node auth stub :3001 Signs and verifies JWTs. The only service that ever sees a password. The stub issues identical claims in Sprints 6 and 7, then is discarded. Application Sprints 6 and 10 Trade REST API Spring Boot · :8080 the write path and the read path verifies the JWT on every /api/** route domain package extension modules · 6 MyBatis mappers producer and consumers One deployable. One JWT filter. One connection pool. Messaging Sprint 7 Event backbone Kafka · :9092 orders · 3 · 7 days trade-events · 3 · 30 days market-data · 6 · 1 day Execution Sprint 7 Trade Executor Java 21 · :8082 group trade-executor · concurrency 3 OrderPlacedListener OrderExecutionService FauxnanceQuoteClient market-data poller Fauxnance API external · 2000 requests a day Data Sprint 3 PostgreSQL 16 :5432 Normalised to third normal form: accounts, instruments, orders, positions, plus the auth schema. Written by the Trade REST API, the Trade Executor and the auth service. Read by everything. Analytical Sprints 4 and 7 Analytics service Python 3.12+ DuckDB file Star schema: dim_account, dim_instrument, dim_date, fact_trades. Loaded in batch, on a schedule, and never on the order path.
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.

The write path, the read path and the analytical read path Three chains of steps. The write path runs from the frontend through the Trade REST API, PostgreSQL, the orders topic, the Trade Executor, PostgreSQL again and the trade-events topic. The read path is a single query answered by the Trade REST API. The analytical path runs from PostgreSQL through the ETL into DuckDB and the dashboard. WRITE PATH · PLACING AN ORDER Frontend POST /api/v1/orders Bearer JWT Trade REST API validate rules 1 to 5 INSERT status NEW PostgreSQL the order row is committed orders key accountId 3 partitions Trade Executor quote, then fill or reject PostgreSQL order, cash, position one transaction trade-events key accountId 30 day retention The HTTP response is sent after step 4, carrying status NEW. Everything from step 5 onwards happens after the user has already seen an answer. READ PATH · REFRESHING THE BLOTTER Frontend GET account orders Bearer JWT Trade REST API verify the token, then check access PostgreSQL SELECT through OrderMapper Trade REST API map rows to OrderHistoryEntry Frontend the blotter shows the new status No broker on the read path. The blotter reads the same rows the executor wrote, which is why a fill appears on a later poll rather than in the original response. ANALYTICAL READ PATH · REPORTING PostgreSQL read only, as analytics_reader Analytics ETL extract, transform, validate, load DuckDB fact_trades and three dimensions Dashboard reads DuckDB from Sprint 7 Runs on a schedule. An analyst running a five-minute aggregate must not be able to slow down order placement, which is the whole reason the split exists.

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.

Trust boundaries The browser zone holds only an access token. The platform zone holds the auth service, which is the only holder of password hashes, the Trade REST API, which verifies the token, the Trade Executor, which is the only holder of the Fauxnance key, and PostgreSQL. The Fauxnance API sits outside the platform. The browser never calls it. BROWSER · UNTRUSTED Frontend Holds the access token, and the refresh token, and nothing else. No password after submit. No database credential. No API key. PLATFORM NETWORK · TRUSTED Auth service the only holder of password hashes argon2 or bcrypt at rest signs the JWT, HS256 Trade REST API verifies the signature locally identity comes from the token authorises per account, per route Trade Executor the only holder of FAUXNANCE_API_KEY no inbound route except health reachable only through the broker PostgreSQL The auth schema is separate from the trading schema. Parameterised statements only, through MyBatis. A least-privilege application role with no DDL rights. Local development runs Kafka in plaintext with no authentication. Document the TLS, SASL and per-topic ACL configuration you would apply in production. Do not claim it is implemented. OUTSIDE THE PLATFORM Fauxnance API X-Api-Key on every request per student, 2000 a day never committed to the repository credentials, once, over HTTPS Bearer token only no password, no key X-Api-Key never: a key sent to the browser is a key you have published
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.

Order placement sequence A sequence diagram across the frontend, auth service, Trade REST API, PostgreSQL, Kafka, Trade Executor, Fauxnance API and analytics service, from login through order acceptance, execution, settlement, the published event and the refreshed blotter. Frontend Angular Auth service NestJS Trade REST API Spring Boot PostgreSQL system of record Kafka event backbone Trade Executor Java 21 Fauxnance API provided Analytics Python, DuckDB 1 · AUTHENTICATE 2 · ACCEPT THE ORDER 3 · EXECUTE IT 4 · PROJECT AND OBSERVE POST /auth/login accessToken, refreshToken POST /api/v1/orders · Authorization: Bearer SELECT account, instrument, position INSERT order, status NEW, unique idempotency_key produce to orders, key accountId 200 · orderId, status NEW consume orders, group trade-executor GET /quotes/{symbol} price, asOf, marketState BEGIN; update the order; move cash; upsert the position; COMMIT produce to trade-events, key accountId consume trade-events, extension module groups batch extract on a schedule GET /api/v1/accounts/{id}/orders 200 · the updated blotter verify the JWT signature locally validate against business rules 1 to 5 in the domain package apply the fill rules against the quoted price load FACT_TRADES in DuckDB
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

Operational and analytical split

Two stores, two models, two access patterns. After Sprint 7 the dashboard never points at the operational database again.

ConcernOperationalAnalytical
StorePostgreSQL 16DuckDB, one file on disk
ModelNormalised to third normal form, per contracts/database-schema.sqlStar schema, per contracts/analytics-schema.sql
Written byTrade REST API, Trade Executor, auth serviceThe analytics ETL only
Read byAll services, and the frontend through their APIsThe dashboard, notebooks, extension analytics
LatencyMilliseconds, single rowSeconds, full scan and aggregate
RetentionCurrent state plus full order historyAppend-only history, dimensions with effective dates
Failure impactTrading stopsReporting 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

BoundaryControlSprint
Browser to any serviceHTTPS in deployed environments, TLS 1.2 minimum11
Frontend to auth serviceCredentials over HTTPS only, argon2 or bcrypt at rest, no password ever logged8
Frontend to Trade REST APISigned JWT in the Authorization: Bearer header, verified on every /api/** route6 with the stub, 8 with the real service
Route to route inside the Trade REST APIEvery 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 PostgreSQLParameterised statements through MyBatis, a least-privilege application role, no DDL rights3 and 6
Any service to KafkaPlaintext 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 APIX-Api-Key from an environment variable. Never commit a key. Never send a key to the browser.7
Deployed frontend to S3Private bucket, origin access control, reachable only through CloudFront11