Modernize a mission-critical legacy payments/settlement core into a cloud-native, event-driven platform without a big-bang cutover, while preserving correctness, auditability, and availability.
| Concern | Target |
|---|---|
| Availability (ledger write API) | 99.99% (≈52 min/year error budget) |
| Ledger post latency | p99 < 250 ms, p50 < 40 ms |
| Throughput | 50,000 TPS sustained peak |
| Durability | Zero acknowledged-then-lost postings (event store is system of record) |
| Recovery | Read model rebuildable from event log; RPO ≈ 0 for the write log |
| Consistency | Strong within an account aggregate; eventual for cross-account read models |
Assume 50,000 TPS peak, average event payload ~500 bytes.
- Write volume: 50,000 events/s x 500 B ≈ 25 MB/s ≈ 2.1 TB/day of raw event log.
- Partitioning: target ≤ 5,000 msg/s per Kafka partition => ≥ 10 partitions on
ledger.events.v1; provision 20 for headroom and consumer parallelism. - Account hot-partitioning: keying by
aggregateId(account) preserves per-account ordering. Hot accounts (e.g. a funding/clearing account) are mitigated with sub-keying (accountId#shard) for the projection consumer while keeping the canonical stream ordered. - Ledger DB writes: one append + one projection upsert per posting. At 50k TPS that is 100k row
ops/s; beyond a single Postgres primary => shard the event store by
aggregateIdhash, or use a log-structured store (e.g. Cassandra/ScyllaDB) for the append-only log with Postgres for projections. - Storage growth: ~2.1 TB/day => tiered retention. Hot (30d) in primary, warm/cold offloaded to the
lakehouse (see
streaming-lakehouse-platform) which also serves analytics and the async projection rebuild.
flowchart TB
client[Clients] --> gw[strangler-gateway]
gw -->|"90% (shrinking)"| legacy[legacy-core]
gw -->|"10% (growing)"| ledger[ledger-service]
gw --> settle[settlement-orchestrator]
legacy -->|outbox CDC| kafka[(Kafka)]
ledger -->|domain events| kafka
settle -->|REST| ledger
kafka --> lake[streaming-lakehouse-platform]
subgraph ledgerInternals [ledger-service]
cmd[Command side: event store] --> proj[Projection: account_balance]
end
- Idempotency check on
paymentId. - Validate invariant (overdraft policy) against read model.
- Append
PaymentPostedto event store (optimistic concurrency via(aggregate_id, sequence_no)). - Update projection; publish event to Kafka.
reserve -> postLedger -> notifyRail -> complete; on rail failure after ledger post => reversePayment
(compensation) => FAILED. State persisted at each step (saga_instance) for crash recovery.
Weighted routing in the gateway shifts /accounts traffic from legacy to ledger-service. Parallel-run
reconciliation compares legacy balances (from CDC) against the new projection before increasing weight.
| Failure | Mitigation |
|---|---|
| Process crash mid-posting | Event store append is the commit point; projection is replayable |
| Dual-write loss (legacy) | Transactional outbox (ADR-0005) |
| Duplicate delivery | Idempotency keys + consumer idempotency (at-least-once) |
| Bad deploy | Canary via Argo Rollouts with SLO analysis auto-rollback (deploy/k8s/canary) |
| Rail outage | Saga compensation reverses ledger; settlement marked FAILED, retried later |
| Read model corruption | Rebuild from event log (POST /api/admin/rebuild-projection) |
- Actuator + Micrometer/Prometheus on every service (
/actuator/prometheus). - Golden signals per service; canary analysis queries error rate and p99 latency directly.
- Correlation id propagated from command through saga and events for end-to-end tracing.