distributed systems · event sourcing · kuum-oss
kuum/Kot.
// async distributed trading engine · Kotlin Coroutines · CQRS · Actor Model

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.

"Deterministic matching: the same event log always produces the same order book."
0node
cluster size
0
events persisted · 1 run
0
trades matched · 1 run
0
race conditions possible
01 system architecture
LAYER I · COMMAND
MatchingActor
Coroutine Channel · Lock-Free
  • One actor per instrument (BTC/USD…)
  • Isolated coroutine — no shared state
  • Idempotency registry deduplicates commands
  • Replay on startup from EventStore
  • 3 overload strategies: Drop / Backpressure / Degrade
  • drain() for deterministic test sync
LAYER II · STORE
EventStore
Append-Only · Optimistic Lock · Flow
  • ConcurrentHashMap per aggregate stream
  • Optimistic locking — version mismatch throws
  • MutableSharedFlow broadcasts to all subscribers
  • Deduplicated via processedIds set
  • FilePersistence via coroutine subscription
  • EventUpcaster for schema migrations
LAYER III · QUERY
QuerySide + Cluster
CQRS · MutableStateFlow · Replication
  • QuerySide subscribes to allEvents() flow
  • Real-time OrderBookView per instrument
  • TradeView ring buffer — last 100 trades
  • ClusterNode: Leader / Follower roles
  • Leader replicates to peers via Channel
  • Live failover: Node-1 → Node-2 at event 500
02 event flow
  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)
03 consistency guarantees
correctness properties · verified by design
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
04 test suite
mvn test · 3 test classes · all pass ✓ ALL PASS
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
4
tests
1000
concurrent orders tested
0
dropped / duplicated
0
race conditions
05 tech stack
core
Kotlin 2.0 Coroutines Channels Flow JVM 11+ Maven
patterns
Event Sourcing CQRS Actor Model Price-Time Priority Double-Entry Accounting Optimistic Locking
testing & observability
JUnit 5 SLF4J FilePersistence MetricsCollector p99 latency
06 quick start
STEP 1 · BUILD
Compile
JDK 11+ · Maven
  • git clone kuum-oss/Kot
  • mvn clean compile
  • no Docker, no external deps
  • pure JVM — runs anywhere
1
STEP 2 · RUN
Simulation
3-node cluster · 1000 orders · chaos
  • mvn exec:java -Dexec.mainClass="org.example.MainKt"
  • Node-1 is leader initially
  • at order 500 — leader switches to Node-2
  • consistency check after: USD/BTC per user per node
2
STEP 3 · TEST
Test Suite
Matching · Concurrency · Replay
  • mvn test
  • 4 tests: matching, priority, concurrent, replay
  • drain() for deterministic async assertions
  • no flakiness — no delay() polling
3
terminal · simulation output
$ mvn exec:java -Dexec.mainClass="org.example.MainKt"
🚀 Cluster started with 3 nodes. Leader is Node-1.
 
[INFO] Metrics: throughput=100 cmds, matches=38, p99 latency=0.21ms
[INFO] Metrics: throughput=200 cmds, matches=74, p99 latency=0.19ms
[INFO] Metrics: throughput=300 cmds, matches=110, p99 latency=0.22ms
[INFO] Metrics: throughput=400 cmds, matches=116, p99 latency=0.20ms
[INFO] Metrics: throughput=500 cmds, matches=118, p99 latency=0.21ms
 
🔥 Chaos: Switching Leader to Node-2
 
[INFO] Metrics: throughput=600 cmds, matches=119, p99 latency=0.23ms
[INFO] Metrics: throughput=700 cmds, matches=120, p99 latency=0.20ms
...
 
✅ Simulation finished.
[INFO] Metrics: throughput=1000 cmds, matches=121, p99 latency=0.21ms
 
📊 Consistency Check:
User a3f1...:
Node-1: USD=97340.00, BTC=10.27
Node-2: USD=97340.00, BTC=10.27
Node-3: USD=97340.00, BTC=10.27
$