Enterprise Trading Platform · architecture diagrams

Frontend

Angular with standalone components and signals. Port 4200 in development, static objects behind CloudFront in production. Four screens, two API clients, one interceptor.

The frontend is the only part of the platform that runs on hardware you do not control, and every design decision here follows from that. It holds a token, not a credential. It attaches that token in exactly one place, so there is one line of code to audit rather than one per request. It guards routes, but treats the guard as a courtesy to the user rather than as security, because the real check happens in the Trade REST API where an attacker cannot edit the code.

There are no NgModules in this project. Providers are registered in app.config.ts and each screen is loaded with loadComponent, so the sign-in bundle does not carry the dashboard, the order ticket and the blotter with it. That matters on a Sprint 11 deployment where the first paint comes over a CDN to a browser that has cached nothing.

Internal structure of the frontend The four routes on the left. Bootstrap, features, guards, interceptors, services, models, helpers and environments in the centre. The auth service and the Trade REST API on the right, both reached from the services layer. INBOUND OUTBOUND the browser one bundle per route, fetched on first navigation to it app.routes.ts /login /dashboard /orders/new /orders Every route except /login is behind authGuard, and an unmatched path redirects to /dashboard. The guard is a courtesy to the user, not a security control. Anyone can edit the bundle in their own browser. The check that counts runs in the Trade REST API. Frontend :4200 dev Angular · TypeScript · standalone components · signals · RxJS · Playwright ui/src/app app Bootstrap. No NgModule anywhere. app.ts · app.html · app.config.ts · app.routes.ts provideRouter(routes, withComponentInputBinding()) · provideHttpClient(withInterceptors(...)) features One folder per screen. login · dashboard · order-ticket · blotter each is a .ts, a .html and a .css, with signals holding component state loaded with loadComponent, so the sign-in bundle carries only sign-in a component calls a service and renders. It never builds a URL or a header itself. core/guards Runs before a route activates. auth-guard.ts · canActivate on every route except /login core/interceptors One line of code to audit. auth-interceptor.ts · the only place the Authorization header is ever set core/services The only layer that makes HTTP calls. auth-service.ts · account-service.ts · order-service.ts order-service re-reads order history while an order is still NEW: every 2000ms, ten attempts core/models Typed from the contracts, not by hand. auth-api.ts · trade-api.ts, generated from docs/contracts/ core Small, shared, no framework dependency. jwt.ts · api-error.ts · validators.ts environments Swapped at build time, not at run time. environment.ts · environment.development.ts, chosen by fileReplacements in angular.json a key added to one file and not the other is a runtime undefined the compiler will not catch Auth service /auth/login, /auth/refresh, /auth/me · authApiBaseUrl Trade REST API /api/v1/** with a Bearer token · tradeApiBaseUrl Two origins, and no others. The frontend never speaks to Kafka, because a browser holds no broker credential, and never calls the Fauxnance API, because a key in a bundle is a key you have published. BUILD CONFIGURATION ng serve → environment.development.ts · ng build → environment.ts, uploaded to S3 and served through CloudFront in Sprint 11 tradeApiBaseUrl · authApiBaseUrl · orderPollIntervalMs = 2000 · orderPollAttempts = 10
File names are the ones in ui/src/app. The two outbound boxes are the only two origins this application is allowed to call.

The four screens

RouteScreenReadsWrites
/loginSign innothingPOST /auth/login, then stores the token pair
/dashboardDashboardaccount, balance, positionsnothing
/orders/newOrder ticketorder history, while the new order is still NEWPOST /api/v1/orders
/ordersBlotterorder historyDELETE /api/v1/orders/{id} to cancel a working order

Handling an order that is not filled yet

From Sprint 7 the API answers POST /api/v1/orders with status: NEW, and the fill lands some time later. The order ticket therefore re-reads order history on an interval, ten times at two second spacing, and then tells the user to check the blotter rather than spinning forever.

Handle both shapes. In Sprint 6 the same endpoint returns FILLED or REJECTED in the original response, and the contract permits both. A screen written against only one of them breaks on the sprint boundary.

What the interceptor is for

One functional interceptor sets the Authorization header, and no component and no service builds that header itself. That gives you one place to add the token, one place to notice a 401, and one place to trigger a refresh. It also means a security review reads one file instead of grepping the whole application for the word "Bearer".