Build Roadmap
This document is the source of truth for the whole site — every other page follows it. Goal: build two simulated core banking systems in Java/Spring, based on an in-depth study of banking business processes and the related financial/distributed system design patterns.
Overview
| Duration | 3–6 months |
| Structure | 2 parallel sub-projects, split by domain |
| Principle | Read the primary sources first → understand the fundamentals → then code |
| Deliverables | 2 GitHub repos + design docs + this documentation (public on VitePress) |
The two banking domains call for quite different design thinking, so they are split into two independent sub-projects instead of one:
- Sub-project B — Core Banking: a "state-centric" domain. The focus is data integrity, ACID, the double-entry ledger.
- Sub-project A — Interbank Payment Gateway: a "flow-centric" domain. The focus is messaging, orchestration, resilience.
Overview diagram
Phase 1 — Months 1–2: Core Banking (Sub-project B)
Starting here because it is the foundational domain, more approachable for someone with a Spring Boot/JPA background, and it prepares the thinking for the more complex Saga work later.
Read first
- Double-entry bookkeeping — the principles of double-entry accounting
- Spring Data JPA — query methods, relationships, transaction boundaries
- Transaction management:
@Transactional, isolation levels, propagation
Build
- Account Service — create/look up accounts
- Ledger Service — double-entry ledger (each transaction posts balanced Debit/Credit entries)
- Transaction Service — internal transactions, keeping balances consistent
Deep dive
- ACID and what it really means in a financial system
- Optimistic vs. pessimistic locking — when to use which
- Audit trail design — why banks never "edit" a record, only "add" offsetting records
DDD refactor — done 2026-09-16
Refactored the Core Banking domain layer towards DDD, in 2 consecutive steps on the same day:
- Money value object — wraps
BigDecimal+Currency,add()/subtract()throwCurrencyMismatchExceptionon mismatched currencies;LedgerEntry/AccountuseMoney/Currencyinstead of rawBigDecimal/String, soLedgerService.validateBalanced()immediately catches transactions mixing currencies instead of silently adding them up wrong. - Transaction aggregate root — a new
Transactionentity (implementsPersistable<UUID>) as the aggregate root for a group ofLedgerEntry, enforcing Debit = Credit in the domain layer rather than only in the service; plus migrationV4__create_transactions_table.sqlandTransactionTest.
Full design decision: ADR-008. Step-by-step log: Devlog — Phase 1.
Outbox Pattern + Kafka event publishing — done 2026-09-17
Notify other systems about Transaction events (posted/reversed) via Kafka without the dual-write risk between the DB write and the message send:
- Outbox Pattern —
Transactionraises domain events,LedgerServicestores them inoutbox_eventsin the same DB transaction as the business data;OutboxEventPublisher(@Scheduled) reads and sends them to Kafka afterwards, making the business write and the event write atomic. - Audit/Compliance consumer — writes one
compliance_recordsrow perLedgerEntry, idempotent via theprocessed_eventstable (deduplicating Kafka's at-least-once delivery). - 3 PoC consumers — Fraud Detection, Notification, Reporting, each with its own
groupIdon the same topictransaction-posted-topic, proving the publish/subscribe architecture works for several independent systems.
Full design decision: ADR-009. Step-by-step log: Devlog — Phase 1.
Redis cache for account balance — done 2026-09-17
Cache-aside for AccountService.getBalance(), without trading away the overdraft invariant guaranteed by pessimistic locking:
- Cache-aside —
AccountBalanceCache(StringRedisTemplate) caches the balance per account with a configurable TTL (default 3600s);withdraw()always computes directly from the DB viacomputeBalanceFromDb(), never reading the cache when checking the invariant. - AFTER_COMMIT invalidation —
AccountBalanceCacheEvictionListenerevicts the cache via@TransactionalEventListener(phase = AFTER_COMMIT), reusing theTransactionPostedEvent/TransactionReversedEventfrom the Kafka feature, narrowing the race window compared with evicting before commit.
Full design decision: ADR-010. Step-by-step log: Devlog — Phase 1.
Oracle dual-profile support — done 2026-09-17
Proving the system runs on enterprise database infrastructure (Oracle) alongside PostgreSQL, from one codebase:
- Dual profile —
db/migration/postgresql/anddb/migration/oracle/fully separated (V1-V7 each), Flywaylocationsconfigured per Spring profile (application.properties/application-oracle.properties); Oracle Database Free in Docker, port 1522. - 7 migrations rewritten for the Oracle dialect — UUID →
RAW(16),UPDATE ... FROM→ correlated subquery /MERGE INTO,JSONB→JSON, partial index → function-based index. - Found & fixed a Money bug — running the tests on Oracle revealed
Moneydid not normalize its scale (OracleNUMBERdoes not keep a fixed scale like PostgresNUMERIC);scale = 2is now fixed for every currency inMoney.java. - Result: 45/45 tests pass unchanged on both databases, with no profile-specific test changes.
Full design decisions: ADR-011 (dual profile), ADR-012 (Money scale fix). Step-by-step log: Devlog — Phase 1.
Kubernetes deployment — done 2026-09-17
Verifying pessimistic locking (against overdraft) holds with several application instances in parallel, through a real K8s deployment:
- REST controller — added
AccountController(there was no HTTP endpoint before), required to verify locking across several Pods (separate JVMs) instead of calling the service in-process like the existing tests. - Dockerfile + Kafka listener fix — multi-stage build; a second Kafka listener (
PLAINTEXT_HOST, viahost.docker.internal) becauselocalhostinside a container is not the host. - K8s deployment —
Namespace/ConfigMap/Secret/Deployment/Service(NodePort) on Docker Desktop Kubernetes, with the data layer (Postgres/Oracle/Kafka/Redis) still in docker-compose outside the cluster. - Experimental verification: scaled to 3 replicas and fired 5 concurrent
withdrawrequests — 1 succeeded, 4 were rejected with HTTP 409, andkubectl logsconfirmed the requests really spread across all 3 Pods (not routed to one place by chance).
Full design decision: ADR-013. Step-by-step log: Devlog — Phase 1.
Phase 2 — Months 3–4: Interbank Payment Gateway (Sub-project A)
Read first
- ISO 20022 message structure —
pain.001(customer credit transfer initiation),pacs.008(interbank credit transfer) - Saga pattern — orchestration vs. choreography
- Spring State Machine / Spring Batch docs
Build
- Message parsing & validation for pain.001 / pacs.008
- Saga orchestration for the interbank payment flow
- End-of-day (EOD) batch settlement with Spring Batch
- Exception/ops handling — failed transactions, retry, dead-letter
Deep dive
- The distributed transaction problem — why 2PC does not fit large-scale distributed systems
- Compensating transactions — how "undo" in a distributed system differs from a normal rollback
- Idempotency — why a message sent twice must never create two transactions
Phase 3 — Months 5–6: Integration & polish
- Connect the 2 sub-projects: the Payment Gateway calls Core Banking to update real balances
- Add Spring Security to the APIs (authentication/authorization)
- Thorough testing: unit tests + integration tests
- Write the README / design docs — explaining the architecture decisions
- Summarize the reasoning behind the main architecture decisions: why Saga instead of 2PC, why a double-entry ledger, why two separate sub-projects...
Learning method
- Learn first, code second: every major module starts by reading the primary sources (Spring docs, the ISO 20022 spec) — not to "know about" them, but to understand why things are designed that way.
- Handwritten notes in parallel: a notebook split into two parts —
- Concepts learned: general knowledge, reusable later
- Project log: architecture decisions, problems met, how they were solved — the raw material for the design docs
- Depth over speed: the 3–6 month timeline is an estimate, not a hard deadline.
Progress log
This section is updated over time as each phase is implemented. Per-phase details are in the Devlog.
- [x] Phase 1: Core Banking
- [ ] Phase 2: Interbank Payment Gateway
- [ ] Phase 3: Integration & polish