- Published on
Designing Software That Explains Itself
5 min read
- Authors
- Name
- Shuwen
Table of Contents
- Designing Software That Explains Itself
- 1. Layered MVC Works Until It Doesn't
- 2. A Concrete Example: Order Checkout
- 3. The Real Problem: The Use Case Is Invisible
- 4. What This Feels Like for Developers
- 5. Architecture Is About Communication
- 6. How Clean and Hexagonal Architecture Fix This
- 7. Why This Scales Better Over Time
- 8. Diagrams to Include
- Final Takeaway
Designing Software That Explains Itself
Recently, I worked on a legacy backend system where the architecture followed a classic MVC pattern: controllers, services, repositories. The entry point was always a controller, so I started there. To add a new feature, I created a few new services, wired them together, and built a workflow that touched several existing components. It worked. Everything compiled. Tests passed. From a purely technical perspective, nothing was wrong.
But before I could even write that first line of code, I spent several hours just trying to understand the system. I had to trace the workflow from controller to service, from one service to another, then down to repositories, piecing together the business logic in my head. There was no place in the codebase that clearly said, "This is what this application does." There were no explicit use cases, only scattered logic across folders named service, impl, and repository.
When I finished the feature, I realized something uncomfortable: the next developer would probably do exactly what I just did. They would scan the controllers, follow method calls across services, guess where new code should live, create another interface, another implementation, maybe another folder, and move on. Over time, more developers would repeat this pattern. The code would still work, but understanding it would become harder and harder. The architecture allowed this drift because it never clearly expressed the system's intent.
That experience pushed me to refactor the project toward a hexagonal, clean architecture. Instead of hiding behavior inside layers, I made the use cases explicit. Once that was done, the difference was immediate. New developers could understand the system by reading the use case names. Features had obvious places to live. The code did not just work, it explained itself. That is why I am writing this article.
Most software systems do not fail because they are slow. They fail because developers can no longer understand them or safely update the code.
At the beginning, everything feels fine. The controllers are clean. The services are organized. The repositories do their job.
Then the system grows. New features arrive. More developers join. More folders appear. And slowly, something disappears:
No one can clearly explain what the system actually does anymore.
This article is about why that happens in layered MVC architecture, and how designing around explicit use cases fixes it.
1. Layered MVC Works Until It Doesn't
Layered architecture (Controller -> Service -> Repository) is popular for a reason:
- It is easy to learn.
- It looks clean.
- It scales well at first.
For small systems or CRUD-heavy applications, MVC is often the right choice. The problem does not appear immediately. It appears when the software grows.
2. A Concrete Example: Order Checkout
Imagine a simple checkout system:
controller/
OrderController
service/
OrderService
repository/
OrderRepository
model/
Order
Initial requirement: "Create an order and charge the customer."
Everything is clear. The flow is easy to follow.
Now the system evolves:
- Promo codes
- Inventory reservation
- Fraud checks
- Payment retries
- Idempotency
- Outbox events
- Multiple entry points (REST, Kafka, scheduled jobs)
Nothing unusual. Just real software.
3. The Real Problem: The Use Case Is Invisible
Layered MVC does not explicitly show business use cases. There is no single place called:
- CheckoutOrder
- RetryPayment
- ExpireUnpaidOrders
Instead, the checkout use case is:
- Partially in OrderService
- Partially in PaymentService
- Partially in InventoryService
- Partially in controller logic
The use case exists, but only in people's heads.
4. What This Feels Like for Developers
When a new developer joins the team and asks, "Where is the checkout logic?" the honest answer is:
"Well, it is spread across several services. You will need to read a lot of code."
This creates real pain:
- Developers must reverse-engineer behavior.
- Nobody knows where new code belongs.
- Changes feel risky.
- Architecture stops guiding decisions.
Over time, people just add new folders, add new services, and make it work. The repository becomes technically organized but conceptually unclear.
5. Architecture Is About Communication
Architecture is not just about layers. It is about explaining intent.
Good architecture should answer, quickly:
- What is this system for?
- What are its core use cases?
- Where do I add a new feature?
Layered MVC answers how the code is structured. It does not clearly answer why the system exists.
6. How Clean and Hexagonal Architecture Fix This
Clean architecture starts from one assumption: use cases are first-class citizens.
Instead of organizing code by technical layers, we organize it by behavior:
application/
usecase/
CheckoutOrderUseCase
RetryPaymentUseCase
ExpireUnpaidOrdersUseCase
domain/
Order
BusinessRules
adapters/
in/ (REST, Kafka, jobs)
out/ (DB, payment, Redis)
Now:
- Every feature has a clear home.
- Business intent is visible.
- New developers can understand the system by reading folder names.
7. Why This Scales Better Over Time
When you add a new feature, you add a new use case. Not a random service or another folder guess.
When infrastructure changes, Kafka instead of REST, Redis instead of DB, new payment provider, your core business logic stays stable.
That is the real power.
8. Diagrams to Include
Layered MVC (use case is hidden):
Controller
|
Service A -> Service B -> Service C
|
Repository
Clean architecture (use case is explicit):
[ CheckoutOrderUseCase ]
^ ^
REST Kafka
| |
PaymentPort InventoryPort
Final Takeaway
Layered MVC is not wrong. It is just not designed to communicate intent.
As systems grow, understanding becomes more important than structure.
Clean architecture works because it makes the most important thing obvious: what the system is actually built to do.
