Personal · Open source
Trading Simulator
A stock exchange simulator: four Spring Boot services, a lock-free matching engine with one order book per symbol, and Kafka events delivered through a transactional outbox.
Problem
An exchange has to match orders fairly and quickly, never let a trader spend the same cash or shares twice, and keep every service consistent even when messages arrive twice or Kafka goes down.
What I built
- User service: authentication, JWT and accounts, with an admin role
- Order service: order validation, funds reservation and a market simulator that generates order flow
- Matching engine: one order book per symbol, with price-time priority, market protection and self-trade prevention
- Portfolio service: trade settlement, holdings, cash and PnL
- A React and TypeScript admin dashboard for operating the exchange
- Monitoring with Prometheus and Grafana, and the whole stack running in Docker Compose
Technical highlights
- Limit and market orders. Market orders are immediate-or-cancel and capped at ±5% of the reference price, like NSE's market protection
- Order books are rebuilt from PostgreSQL on restart
- Admin APIs and the admin WebSocket feed require a JWT with the ADMIN role
- Java 21 virtual threads, a multi-module Maven build, and SQL migrations that are safe to re-run
Key decisions
- 01
One thread per symbol, no locks
Each symbol has its own engine on a virtual thread, fed by a queue. Only that thread ever touches the symbol's order book, so matching needs no locks.
- 02
Transactional outbox for Kafka
Services never publish to Kafka inside a transaction. The event is written to an outbox table in the same transaction as the change, and a relay publishes it after commit, with retries. Consumers never see an event for a rolled-back change, and a Kafka outage delays events instead of losing them.
- 03
Idempotent consumers
Delivery is at-least-once, so every consumer tolerates duplicates. Settlements are recorded in the same transaction as the cash change, and the engine ignores orders it has already handled.
- 04
Available funds are derived, not stored
Available cash and shares are computed from holdings minus open orders and unsettled trades. Fills, cancels and settlement update the source rows in one transaction, so there is nothing to release and nothing to leak.
- 05
Cancels finish in the engine
A cancel first marks the order CANCELLING, which keeps its funds reserved. The engine then removes it, or it ends FILLED if a fill got there first, so a cancel can never free funds a queued fill is about to use.
Hardest problems
- Matching orders concurrently without locks or race conditions
- Keeping cash and shares consistent across services with at-least-once delivery
- Racing cancels against fills without leaking or double-spending funds
Architecture
- React admin dashboard
- User · Order · Matching · Portfolio services
- Kafka via transactional outbox
- PostgreSQL · Redis
- Prometheus · Grafana
Stack
- Java 21
- Spring Boot 3
- Kafka
- PostgreSQL
- Redis
- React
- TypeScript
- Prometheus
- Grafana
- Docker