Enterprise Trading Platform · architecture diagrams

Trade REST API

Java 21, Spring Boot, MyBatis, port 8080. The service that accepts orders, records them, publishes them, and answers every question the frontend asks about an account.

This is the largest of the six and the one that grows most. It starts in Sprint 6 as a controller, a service layer and a set of mappers. Sprint 5's domain model is absorbed into it as a source package rather than a separate artifact, so a clean checkout builds with one command. Sprint 10 adds the extension modules as further packages inside the same deployable, each with its own routes and its own consumer group.

Read the diagram top to bottom. A request enters at web, is authenticated at security, is orchestrated at service, is decided at domain, is persisted at repository and is published at messaging. The direction never reverses. The domain package holds no Spring annotation beyond validation and performs no I/O, and that rule is now enforced by review rather than by a Maven boundary.

Internal structure of the Trade REST API Inbound HTTP routes from the trade and portfolio contracts and inbound Kafka topics on the left. Eight internal packages in the centre: web, security, service, domain, extensions, repository, messaging and config. Outbound PostgreSQL and the orders topic on the right. The environment variables the service reads run along the bottom. INBOUND OUTBOUND contracts/trade-api.yaml POST /api/v1/orders DELETE /api/v1/orders/{id} GET /api/v1/accounts/{id} GET .../{id}/balance GET .../{id}/positions GET .../{id}/orders contracts/portfolio-api.yaml GET /api/v1/portfolio/{id} GET .../{id}/positions GET .../{id}/pnl operations, not in a contract GET /actuator/health GET /swagger-ui consumed by extension modules trade-events market-data one group id per module Every inbound HTTP route carries a JWT. The filter runs before any controller, and identity comes from the verified token, never from a path or query parameter. Trade REST API :8080 Java 21 · Spring Boot 3.x · MyBatis 3.5 · Bean Validation com.tradingplatform.tradeapi web HTTP in, JSON out. No SQL, no domain rule. OrderController · AccountController · GlobalExceptionHandler dto: OrderResponse, PositionResponse, BalanceResponse, OrderHistoryEntry, ErrorResponse security Verifies the signature. Does not issue tokens. JwtAuthenticationFilter · JwtVerifier · AuthenticatedUser InvalidTokenException · AccountAccessDeniedException service Transaction boundary and orchestration. OrderService · AccountService · ConcurrentUpdateException domain Built in Sprint 5, absorbed as source in Sprint 6. model: Account, Instrument, Order, Position, Money, OrderSide, OrderStatus, AccountStatus service: OrderPlacementService, SettlementService, IdempotencyKeyRegistry exception: TradingException and its subclasses · dto: PlaceOrderRequest extensions Sprint 10. One package per extension. portfolio · watchlist · notification · preferences · advice · strategy each with its own routes, its own service layer and, where it needs one, its own consumer group ids stay distinct: portfolio-service, watchlist-service, notification-service, advice-service, strategy-service repository The only package that knows SQL exists. AccountMapper · InstrumentMapper · OrderMapper · PositionMapper UuidTypeHandler · UtcInstantTypeHandler · resources/mapper/*.xml messaging Publishes after the commit, never inside it. KafkaEventPublisher · EventEnvelope · OrderPlacedPayload · TradeEventPayload acks=all · enable.idempotence=true · max.in.flight.requests.per.connection=5 config Cross-cutting. Read once, at start-up. TradingProperties · SecurityFilterConfig · MyBatisConfig · DomainConfig ClockConfig · OpenApiConfig · ExecutionMode PostgreSQL accounts, instruments, orders, positions parameterised statements only orders key accountId, as a string eventType ORDER_PLACED This service does not call the Fauxnance API. Prices reach it on market-data, which is why it needs no key of its own. ENVIRONMENT SERVER_PORT · DB_HOST · DB_PORT · DB_NAME · DB_USER · DB_PASSWORD · DB_POOL_SIZE · KAFKA_BOOTSTRAP_SERVERS · JWT_SECRET JWT_CLOCK_SKEW_SECONDS · JWT_REQUIRED_ISSUER · TRADING_EXECUTION_MODE · TRADING_BASE_CURRENCY · TRADING_KAFKA_ENABLED · KAFKA_ORDERS_TOPIC · KAFKA_TRADE_EVENTS_TOPIC
Package names and class names are the ones in the source tree. The extension packages are the ones a team creates in Sprint 10, one per capability it picks from the catalogue.

The two execution modes

TRADING_EXECUTION_MODE decides what POST /api/v1/orders returns, and it is the single switch that separates Sprint 6 from Sprint 7. Set it deliberately and know which one you are running.

ModeSprintWhat happensResponse
sync6Validate, settle in process, write the order and the position, return. No broker involved.FILLED or REJECTED
async7 onwards, the defaultValidate, insert the order, publish to orders after the commit, return.NEW

What this service owns and what it does not

OwnsDoes not own
Validating an order against the five business rulesDeciding the fill price. That is the executor's job, against a live quote.
Recording the order with a unique idempotency_keyMoving cash or updating a position on a fill. That happens in the executor's transaction.
Publishing ORDER_PLACED to ordersPublishing to trade-events. It consumes that topic, it does not write to it.
Verifying the JWT on every /api/** routeIssuing or refreshing a JWT, and hashing a password.
Serving the portfolio routes from the extension modulesHolding a Fauxnance key. It has no reason to call that API.

Where extension modules change the security problem

In a separate service a team could lean on the service boundary for authorisation. Inside one application there is no boundary to lean on. Every extension route enforces its own account check, and a route that returns another customer's portfolio is now a defect in the same process that holds the order book. The Sprint 10 security review has more to find, not less.