If you read any blogs of mine, i assume that you already understood how obsessed i am with system design and design patterns but it doesn't mean i know every single thing around that study. So, in one of my shiny days, i was busy with reading a book named Clean Architecture: A Craftsman's Guide to Software Structure and Design by Robert Cecil Martin.
He was talking about what does clean architecture mean actually and why this is important for a developer. After finishing the book, i was like "This is awesome but probably i forget all this information if i don't create any project about this architecture."
Thanks to Uncle Bob, i have created the Ledger project and i'm excited to share what i've learned.
The Ledger project is a modular monolithic financial ledger system designed for atomic transactions, consistent double-entry accounting, and high-concurrency reliability. It simulates real-world banking operations where data integrity and race conditions are the primary concerns. By implementing double-entry bookkeeping and pessimistic locking, it ensures that not a single cent is lost, even under concurrent load.
While reading Clean Architecture, Uncle Bob’s principle that 'Details (DB, UI, Frameworks) should not dictate our business rules' really stuck with me. In this project, I treated PostgreSQL, Kafka, and Redis as mere 'tools' or 'details.' The core business logic doesn't even know they exist, it only communicates through ports. This separation of concerns gave me an incredible sense of flexibility and confidence in the system's stability.
Transactional Outbox Pattern
I also implemented Transactional Outbox Pattern, ensured reliable event delivery to Apache Kafka by using an Outbox Pattern. It means i enabled saving the event in the same database transaction as the business logic, i guaranteed that events are never lost, even during network partitions.
Optimistic/Pessimistic Locking
And another thing i want to share with you is Optimistic/Pessimistic Locking. Let's imagine there are multiple users try to transfer money from the same account at the same time, how do we prevent the "Double Spending" problem? This is where locking mechanisms come in.
- Optimistic Locking: Optimistic locking assumes that conflicts are rare, It uses a @Version column in the database. When a transaction commits, it checks if the version has changed. It usually has better performance in low-contention scenarios because there are no database-level locks.
- Pessimistic Locking: If you "Safety First!" then this is for you. Pessimistic locking assumes conflicts will happen. It uses SELECT ... FOR UPDATE to lock the account record as soon as transaction starts. No other transaction can even read that record for update until the first one is finished. It guarantees consistency.
It shouldn't be a surprise why I've chosen Pessimistic Locking. In a financial system, consistency is non-negotiable. No need for explaining why this one, we are talking about someone's money...
Deep Dive into Clean Architecture: Why and How?
Why did I go through the trouble of separating everything into so many layers? Isn't it "over-engineering"? After building the Ledger project, my answer is a big no!
First, we should understand what "Core Design" is. The design is built around the Dependency Rule which is source code dependencies must point only inward, toward high-level policies.
In the Ledger project, this looks like a set of contentric circles:
- Entities (The Heart): Account, Transaction, and Entry. These contain the core financial rules, they don't have any idea about Spring, JPA, or Kafka.
- Use Cases (The Brain): ExecutreTransferUseCase, CreateAccountUseCase: These coordinate the flow of data to and from entitites.
- Interface Adapters (The Translators): This is where our Controllers and Persistence Adapters live. They translate data from the outside into a format the Use Cases understand.
- Frameworks & Drivers (The Tools): Spring Boot, PostgreSQL, Kafka. Tese are at the outrmost edge, they are "details" that can be swapped.
So, why did i use it for Ledger?
- Screaming Architecture: When you look at the package structure, it "screams" Fintech/Ledger, Not SpringBootApplication. You see Transfer, Account and Bookkeeping before you see any technical details.
- Testability without Borders: I can test the entire money transfer logic using simpe Unit Tests and Mocks. I don't need to start a database or a web server to verify that my accounting rules are correct.
- Protecting the Business Value: In a financial system, the business rule are the most valuable part. Clean Architecture acts as a shield, protecting these rules from being pullted by technical changes in libraries or frameworks.
To clarify what are the benefits of Clean Architecture, i have created a table for you:
| Feature | Traditional Layered (LA) | Clean Architecture (CA) |
|---|---|---|
| Dependency D. | Top-down (UI -> Service -> DB) | Inward (Adapters -> Use Cases -> Entities) |
| Business Logic | Often coupled with DB/Frameworks | Completely isolated in the core |
| Database Swap | Hard; logic depends on the DB schema | Easy; DB is just a "detail" adapter |
| Testability | Requires DB/Spring context (Slow) | Pure Unit Tests (Fast & Independent) |
| Screaming Design | Shows "Spring/Web" structure | Shows "Business/Domain" intent |
| Maintenance | Changes in DB ripple through all layers | Changes in DB only affect its adapter |
| Initial Effort | Low (Fast to start) | Medium (Requires more boilerplate) |
Is it excellent, isn't it? Yes i feel same. I'm still learning every day and this project was a milestone for me to understand the concept. Thanks for joining me on this deep dive.