I've been practicing Clean Architecture for years. Topo Contábil has 252 use cases following the pattern strictly. Living with it at that scale has taught me things I hadn't picked up from Uncle Bob's book: where it helps and where it gets in the way.
What works
Testability is the clearest win. Every use case is isolated from its dependencies, which come in through injection. In tests, mocks go in and I assert on the output. Our unit test suite runs in 18 seconds because nothing touches a database or network. Each use case has one responsibility and its own test file, so a failure points to exactly what broke.
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?" Input validation in the DTO, business rules in the entity. Those locations are stable.
Injecting dependencies through interfaces gives me confidence to refactor. 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.
Separating domain entities from database entities forces you to think about the domain. Our fiscal document entity has behavior to validate itself, calculate totals and transition states. It's not a bag of properties that gets mutated from the outside. This helps with the complexity of Brazilian tax law.
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 only exists for 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 habit pushes you to add one anyway. I did, and now there's 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. Reads don't need to go through the same path as writes.
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.