A high-throughput trading engine built on Event Sourcing, CQRS, and the Actor Model.
Each instrument runs in an isolated Coroutine actor — no locks, no shared mutable state,
no race conditions. The full system state is reproducible from the append-only event log
at any point in time. A 3-node cluster simulation with live leader failover shows
321 events — 200 placements, 121 matches — across BTC/USD in a single run.
PlaceOrder command │ ▼ SecurityGateway ── authenticate() + checkRateLimit() │ ▼ ClusterNode (Leader only — followers reject) │ ▼ TradingEngine ── getOrCreate MatchingActor[instrumentId] │ ▼ MatchingActor.send() ──► coroutine Channel │ │ (single goroutine processes sequentially — no locks needed) ▼ ┌─ idempotency check ──── duplicate? → skip │ ▼ BalanceService.getAccount() ── insufficient funds? → return │ ▼ BalanceLocked event generated │ ▼ PriceTimePriorityStrategy.match() │ OrderBook (TreeMap: bids DESC, asks ASC) │ iterate opposite side until price no longer matches │ ├── match found → OrderMatched event └── always emit → OrderPlaced event (taker) │ ▼ EventStore.append(instrumentId, expectedVersion, events) │ optimistic lock — version mismatch → retry │ emit to MutableSharedFlow │ persist via FilePersistence │ ├──► OrderBook.applyEvent() (command side state) ├──► BalanceService.applyEvent() (double-entry accounting) ├──► QuerySide.applyEvent() (read model: OrderBookView, TradeView) └──► ClusterNode peers via replicationChannel (followers)
| property | mechanism | where in code |
|---|---|---|
| No race conditions | Single coroutine per actor | MatchingActor — Channel processes sequentially, no synchronization needed |
| Deterministic matching | Price-Time Priority | OrderBook — TreeMap bids DESC / asks ASC, FIFO within same price |
| Double-Entry Accounting | Lock / Unlock before match | BalanceService — BalanceLocked before order placed, BalanceUnlocked on cancel |
| Exactly-once processing | Idempotency registry | MatchingActor.idempotencyRegistry — command.id checked before handleCommand() |
| Optimistic concurrency | Version check on append | InMemoryEventStore — expectedVersion must match currentVersion or throws |
| Full state replay | Event sourcing | MatchingActor.init — replays full stream before accepting commands |
| Schema evolution | EventUpcaster | DefaultEventUpcaster — pluggable migration hook on getStream() |
| Fault tolerance | Leader election + replay | ClusterNode — follower replays from replicationChannel after failover |
| test | what it verifies | result |
|---|---|---|
| MatchingTest · price-time priority | BUY 100@10, SELL 50@9 matches at maker price 10 · partial fill · full fill · book empty | ✓ PASS |
| MatchingTest · price priority for asks | Two sells at 90 and 100 — BUY 15@110 matches 10@90 first, then 5@100 (best price wins) | ✓ PASS |
| ConcurrentTradingTest · 100 users × 10 orders | 1000 concurrent coroutines → single actor → all 1000 OrderPlaced in event store, no drops, no duplicates | ✓ PASS |
| ReplayTest · state restoration | Pre-seed BUY order in store → start fresh actor → actor replays → SELL matches → 4 events total including OrderMatched | ✓ PASS |