root@construct:~/logs/clean-architecture-after-252-use-cases$
<-- back to /logs
2025-12-20//LOG

Clean Architecture After 252 Use Cases

I've been practicing Clean Architecture for years. TOPO Contabil has 252 use cases following the pattern strictly. Living with it at that scale has taught me where it helps and where it gets in the way.

What works

Testability. This is the undeniable win. Every use case is a pure function of its dependencies. Inject mocks, assert outputs. Our unit test suite runs in 18 seconds because nothing touches a database or network. When a test fails, you know exactly what broke because the scope is tiny. Each test file covers a use case with a single responsibility.

Onboarding. New developers are productive in days, not weeks. The structure is so predictable that once you've seen one module, you've seen them all. "Where does the business logic live?" In the use case. "Where does the database logic live?" In the repository. "Where does the validation live?" In the DTO. Those locations never change.

Refactoring confidence. When every dependency is injected through an interface, swapping implementations is trivial. We migrated from one tax calculation provider to another by writing a new adapter. Neither the business logic nor the tests needed changes. The use cases didn't know or care.

Domain modeling. Forcing yourself to separate domain entities from database entities makes you think about your domain. Our fiscal document entity has behavior. It can validate itself, calculate totals, transition states. It's not a bag of properties that gets mutated from the outside. This matters when your domain is complex.

What gets in the way

The ceremony. Creating a new feature means creating a minimum of 5 files. Use case, input DTO, output DTO, controller method, maybe a new repository method. For a simple CRUD endpoint this is overkill. We've accepted this tax because the benefits at scale outweigh the cost, but I won't pretend it doesn't feel heavy sometimes.

Mapper hell. Converting between domain entities, database entities, DTOs, and response objects means writing a lot of mapping code. It's tedious and error-prone. We've automated some of it but there's still a mapper layer that exists purely because of architectural purity.

Over-abstraction temptation. When you're in the Clean Architecture mindset, you start abstracting everything. Do you really need a repository interface for a lookup table that will never change providers? Probably not. But the pattern says you should, so you do, and now you have an interface, an implementation, and a module registration for something that could have been a direct database call.

When I break the rules

After 252 use cases, I've developed a sense for when the architecture should flex:

Simple reads that just fetch and return data don't need a full use case. A thin query service that goes straight to the repository is fine. Not everything needs a command/query split.

Prototype features get a simpler structure. When we're not sure a feature will survive, we build it with less ceremony. If it proves valuable, we refactor into full Clean Architecture. If not, it's easy to delete.

Performance-critical paths sometimes need to break the layers. Our report generation module bypasses the repository abstraction and uses raw SQL because the ORM was too slow. The architecture purist in me cries, but the pragmatist knows a 30-second report is worse than an impure layer.

I care about whether the software is easier to maintain. If relaxing an architectural rule helps with that, I will relax it.

252 use cases later, I'd do it all again. But I'd be less dogmatic about it from the start.

The Broad Way | Kinho.dev