This is a Fintech Ledger system.
It focuses on Resilience, Consistency, and High Concurrency.
It is built with Kotlin and Spring Boot WebFlux.
It follows DDD and Hexagonal Architecture principles.
Warning
This repository is a Personal Sandbox & Reference Implementation.
While the code is open for study, I do not accept Pull Requests or provide support.
It serves as a proof-of-concept for modern resilient architecture patterns.
- π About This Project
- πΊοΈ Roadmap & Status
- π Knowledge Lineage & Foundations
- π Key Design Concepts
- π Prerequisites
- π Getting Started
- π§ͺ Running Tests
- π§Ή Code Style & Hygiene
This is a real-world practice project.
I want to master high-performance backend engineering.
And apply the concepts I have learned.
Background:
Coming from a primarily Frontend background and recently transitioned to Backend development.
I realized the need to better my understanding of distributed systems, concurrency, and resilience.
I haven't had many opportunities to touch high-performance backend work in my previous roles.
Goal:
I started this project as a deliberate practice sandbox to rigorously apply concepts I have accumulated from 2023 until now.
My aim is to build a solid foundation for architecting real-world, scalable distributed systems.
- Phase 1: The Core (Domain Logic, Unit Tests, Validation)
- Phase 2: The Architecture (Hexagonal Structure, DB Schema, Outbox Pattern) - π Current Focus
- Phase 3: Resilience (Idempotency, Circuit Breaker, Distributed Tracing)
- Phase 4: Reconciliation (Background Consistency Checks)
- Phase 5: Scaling (Caching, Sharding strategies)
Note
Future Horizons: Once the Wallet core is bulletproof, I plan to introduce other domains like Payment (Orchestration), Accounting (Double-Entry Ledger), and Notification to simulate a full-blown microservices ecosystem
This project is not built in a vacuum. It is the culmination of intensive study and practice in Distributed Systems and Modern Java/Kotlin Development. The architecture is heavily influenced by the following resources:
Important
Defining Performance:
When I say "Performance", I don't just mean nanosecond latency.
I also mean Throughput and Resilience under load.
A fast system that calculates money wrong is just a fast way to go bankrupt.
The guiding principles that shape every line of code.
- High Reasonability & Low Entropy: We hate accidental complexity. Code must be predictable.
- The "War Room" Test (Domain-First):
- During an incident, the Domain Layer must be readable enough that a PO/BA can understand the logic without needing a translator
- β Cryptic (Bad):
if (w.st == 2 && tx.amt > w.bal)
(Dev Speak: Magic numbers, abbreviations, cognitive load) β οΈ Anemic (Better):if (wallet.status == WalletStatus.FROZEN || transaction.amount > wallet.balance)
(Clear Naming: Readable variables, but logic is exposed/leaking)- β
Rich (Recommended):
if (wallet.isFrozen() || wallet.hasInsufficientFunds(transaction))
(Biz Speak: Logic encapsulated, reads like a sentence)
- β Cryptic (Bad):
- Note: Infrastructure details (DB, API calls) are exempt and could be abstracted away
- During an incident, the Domain Layer must be readable enough that a PO/BA can understand the logic without needing a translator
The giants we stand on: Mathematical Predictability, Resilience, Scalability, and Message-Driven Architecture.
- Mathematics for Working Programmers (Series), facilitated by Rawitat Pulam (Lect. Dave)
- Influence: Served as a Thinking Framework grounded in First Principles (Lambda Calculus vs. Turing Machine).
It shifted my paradigm from just writing instruction-based code to designing Logical Structures that are Predictable and Easy to Reason About (Equational Reasoning) - Key Transformation:
- From Accident to Intent: Learned to eliminate ambiguity by replacing complex conditional chains with Clear Predicates and State Transitions, making the business logic explicit and readable
- Flow & Composition: Adopted principles like Low-Entropy and Immutability to reduce cognitive load.
I use concepts from Category Theory (like Functors and Monads) as a practical Design Patterns to handle side effects and data transformation cleanly (e.g., Railway Oriented Programming)
- Influence: Served as a Thinking Framework grounded in First Principles (Lambda Calculus vs. Turing Machine).
- Lightbend Reactive Architecture (Learning Paths), taught by Wade Waldron
- Influence: Provided the foundational mental model for building Message-Driven, Resilient, and Elastic systems.
It established the core principles of the Reactive Manifesto and how to decouple components through asynchrony and isolation - Key Paths & Concepts:
- Foundations Path: Mastered Domain Driven Design (DDD) for modeling bounded contexts and the Hexagonal Architecture pattern, which defines the core structure of this Ledger.
It also emphasized Isolation (State, Space, Time, and Failure) to ensure system-wide resilience - Advanced Path: Focused on Building Scalable Systems, specifically the trade-offs between Consistency and Availability (CAP Theorem) and the Laws of Scalability (Amdahlβs and Guntherβs Laws), which are critical for designing a high-concurrency distributed system
- Foundations Path: Mastered Domain Driven Design (DDD) for modeling bounded contexts and the Hexagonal Architecture pattern, which defines the core structure of this Ledger.
- Influence: Provided the foundational mental model for building Message-Driven, Resilient, and Elastic systems.
The technical skills required to translate architecture into working code.
- Reactive Spring, authored by Josh Long
- Influence: A valuable guide for Testing Reactive Systems and mastering
StepVerifier - Modernization Practice: fResult/Learn-Spring-Webflux-3.0 β I adapted the original Java 17/Maven examples into a Bleeding Edge Stack (Java 24, Spring Boot 3.5-4.0, Gradle Kotlin DSL).
Restructured as a Monorepo with Composite Builds to better understand modern build-tool mechanics
- Influence: A valuable guide for Testing Reactive Systems and mastering
- Learning Spring Boot 3.0, authored by Greg L. Turnquist
- Influence: Offered a practical perspective on "Convention over Configuration" and the Spring Application Context
- Implementation Log: fResult/Learning-Spring-Boot-3.0 β Following the "Get Your Hands Dirty" philosophy, I manually implemented every pattern to internalize the framework's internal mechanics rather than relying on rote memorization
- Gout Together (Leaned from theJava Backend Bootcamp [2024] YouTube Playlist), taught by Thanaphoom Babparn
- Influence (Career): The Critical Milestone that transitioned me from a learner to a capable Java Developer.
By rigorously reviewing concepts, I solidified the Java & Spring Boot expertise required to operate at a professional level - Influence (Technical): The foundational turning point regarding Test Engineering.
It shifted my mindset from a "Coverage-First" lens (satisfying CI gates) to Behavioral Verification.
This series taught me to test complex business logic effectively, ensuring tests provide Real Value rather than just metrics
- Influence (Career): The Critical Milestone that transitioned me from a learner to a capable Java Developer.
This structure separates Business Logic from Infrastructure.
The core domain stays pure and testable.
External changes (e.g. DB or external API) don't affect the core domain.
The project heavily utilizes FP principles, specifically Immutability and Railway Oriented Programming (ROP) via Either.
To understand why we avoid Exceptions for business logic, imagine tracing a critical bug:
1. The "Trapdoor" Nightmare (Exceptions)
In traditional code, a function like chargeUser() might look successful, but deep inside, it throws an Exception.
- The Lie: The signature
fun chargeUser(): Receiptpromises a Receipt, but it might blow up. - The Teleport: The code execution "teleports" (jumps) from
chargeUser()to a hiddencatchblock somewhere far away. - The Pain: It is an invisible trapdoor. You can't see the error path.
2. The "Railway" Clarity (Either)
With Either, the code looks like a linear railway track:
- The Truth: The signature
fun chargeUser(): Either<Failure, Receipt>explicitly states "I might fail" - The Switch: If it fails (returns
Left), the train simply switches tracks to the error line - The Flow: It stays on the rail. No teleportation. No surprises
A common question: "If you catch errors as Left, how does @Transactional rollback?"
The Philosophy: We distinguish between System Errors and Business Results
-
System Failures (e.g., DB Connection Lost):
- These are Exceptions
- They bubble up and trigger
@TransactionalROLLBACK - The state remains consistent (nothing happened)
-
Business Failures (e.g., Insufficient Funds):
- These are Data (
Either.Left)- We COMMIT the transaction
- Why? We must persist the "Rejection Event" (Audit Log/Outbox)
- A bank doesn't pretend a failed transaction never happened, it records a "Declined" entry
- Exploration Area: Transaction Management with
Either&Outbox- Rule: For Business Errors (
Left), NEVER usesetRollbackOnly(). We must commit theFailureEvent. - Rule: For System Errors (Exceptions), allow standard Rollback.
- Rule: For Business Errors (
- These are Data (
In my transition from Frontend to Backend, I found that traditional exception handling often created "Hidden Control Flows."
The function signature fun process(): Wallet implies guaranteed success, but it effectively "lies" if it throws a runtime exception.
By adopting Railway-Oriented Programming, we force the function signature to tell the truth: fun process(): Either<Failure, Wallet>.
This explicitly states, "I might fail, and here is exactly how," forcing the caller to handle errors as Domain Data rather than unexpected crashes.
Instead of guessing where the code might crash, we write code that reads like a business flowchart:
// Real-world code that acts as documentation
fun processPayment(cmd: PaymentCommand): Mono<Either<Failure, PaymentReceipt>> {
return validateRequest(cmd) // 1. Validate
.flatMap(::deductBalance) // 2. Deduct Balance (Switch track if logic fails)
.flatMap(::callBankApi) // 3. Call Bank API (Switch track if network fails)
.flatMap(::saveTransaction) // 4. Save Transaction
.flatMap(::sendEmail) // 5. Notify User
}Install these tools before you start:
- JDK 24: This project uses Java 24
- Docker Engine: Colima is recommended on macOS/Linux for better performance
- Docker Compose: Required for running the PostgreSQL database
- Git: Used for version control
This project uses a pre-commit hook to enforce code style.
Run this command once to install it:
./gradlew installGitHooksRun this command if you need to format code by your hand:
./gradlew spotlessApplyIf you use macOS with Colima, start it first.
Make sure you give it enough CPU and RAM.
# Start colima with 4 CPUs and 8GB RAM (Adjust as needed)
colima start --cpu 4 --memory 8Use Docker Compose to start the database:
# Start PostgreSQL database
docker compose up -dRun the Spring Boot application via Gradle wrapper:
./gradlew bootRunThe server will start on port 8080 (Default).
This project is tested heavily.
It uses Unit and Integration Tests.
Important
Integration Tests utilize Testcontainers, which requires a running Docker environment.
If you use Colima, ensure it is running (colima start) before executing tests.
./gradlew test
β οΈ Note: Requires jq installed (brew install jq).
FIXME: Fix this test command as can't run correctly.
./gradlew --stop &&
# Verify Colima is running and pass the socket/host explicitly
TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock \
TESTCONTAINERS_HOST_OVERRIDE=$(colima ls -j | jq -r '.address') \
DOCKER_HOST="unix://$HOME/.colima/default/docker.sock" \
./gradlew testTip
Need a visual guide?
Check out my step-by-step article with screenshots on Medium:
Fix .env File Not Loaded in IntelliJ Tests
For a faster feedback loop and seamless .env file support, we recommend switching the test runner from Gradle to IntelliJ.
This avoids Gradle daemon overhead for local testing.
- Open Settings (or Preferences on macOS)
- Go to
Build, Execution, Deployment>Build Tools>Gradle - Change Run tests using from Gradle to IntelliJ IDEA
To ensure Testcontainers picks up environment variables automatically for every new test:
- Open Run/Debug Configurations (top right toolbar)
- Click Edit Configuration Templates... (bottom left link in the dialog)
- Select JUnit from the list
- Enable the EnvFile tab (install the plugin if missing)
- Check
Enable EnvFile - Click
+and attach your.envfile - Click Apply/OK
Tip
Why do this?
The Native IntelliJ Runner is significantly faster for repeated test execution and the EnvFile plugin allows you to inject local Docker/Colima configurations without messy command-line arguments.
Code style is enforced to ensure consistency.
The project uses Spotless, Ktlint, and EditorConfig.
A Pre-commit Hook checks files before committing.
To manually format the code:
./gradlew spotlessApply