Skip to content

fResult/resilience-wallet-ledger

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

162 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Resilience Wallet Ledger

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

⚠️ Educational Purpose Only
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.

πŸ“‘ Table of Contents

πŸ“– About This Project

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.

πŸ—ΊοΈ Roadmap & Status

  • 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

πŸ“š Knowledge Lineage & Foundations

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:

🧠 Architectural Foundations (The "Why")

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.

πŸ§˜β€β™‚οΈ Core Philosophy

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)
    • Note: Infrastructure details (DB, API calls) are exempt and could be abstracted away

πŸ›οΈ External Influences & Roots

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)
  • 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

πŸ›  Implementation Mastery (The "How")

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
  • 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

πŸ› Key Design Concepts

Hexagonal Architecture + Domain-Driven Design (DDD)

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.

Ζ› Functional Programming (FP)

The project heavily utilizes FP principles, specifically Immutability and Railway Oriented Programming (ROP) via Either.

πŸ“– The Visual Analogy: "Trapdoor" vs. "Railway"

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(): Receipt promises a Receipt, but it might blow up.
  • The Teleport: The code execution "teleports" (jumps) from chargeUser() to a hidden catch block 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

πŸ’Ύ The "Commit the Failure" Strategy (Transaction Management)

A common question: "If you catch errors as Left, how does @Transactional rollback?"

The Philosophy: We distinguish between System Errors and Business Results

  1. System Failures (e.g., DB Connection Lost):

    • These are Exceptions
    • They bubble up and trigger @Transactional ROLLBACK
    • The state remains consistent (nothing happened)
  2. 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 use setRollbackOnly(). We must commit the FailureEvent.
      • Rule: For System Errors (Exceptions), allow standard Rollback.

🧠 Personal Reflection: Why Either?

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.

πŸ’‘ The Real-World Implementation

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
}

πŸ“‹ Prerequisites

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

πŸš€ Getting Started

1. Setup Git Hooks

This project uses a pre-commit hook to enforce code style.

Run this command once to install it:

./gradlew installGitHooks

Run this command if you need to format code by your hand:

./gradlew spotlessApply

2. Setup Docker Environment (Colima)

If 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 8

3. Start Infrastructure

Use Docker Compose to start the database:

# Start PostgreSQL database
docker compose up -d

4. Run the Application

Run the Spring Boot application via Gradle wrapper:

./gradlew bootRun

The server will start on port 8080 (Default).

πŸ§ͺ Running Tests

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.

πŸ”§ CLI (Terminal)

Standard Run

./gradlew test

For Colima Users

⚠️ 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 test

πŸ”§ IDE Configuration (IntelliJ IDEA) recommended

Tip

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.

1. Switch Test Runner to IntelliJ

This avoids Gradle daemon overhead for local testing.

  1. Open Settings (or Preferences on macOS)
  2. Go to Build, Execution, Deployment > Build Tools > Gradle
  3. Change Run tests using from Gradle to IntelliJ IDEA

2. Configure EnvFile Support (Global Template)

To ensure Testcontainers picks up environment variables automatically for every new test:

  1. Open Run/Debug Configurations (top right toolbar)
  2. Click Edit Configuration Templates... (bottom left link in the dialog)
  3. Select JUnit from the list
  4. Enable the EnvFile tab (install the plugin if missing)
  5. Check Enable EnvFile
  6. Click + and attach your .env file
  7. 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 & Hygiene

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

About

Practice Real-World Project for Distributed Systems using Kotlin, Vavr, and Spring Webflux

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages