Loading...
Journal

The Cargo Cult of Clean Architecture

8 min
1,600 words
EngineeringArchitectureComplexityPatterns

We layer our applications like onions, convinced that one day we'll swap the database. A look at why 'Clean Architecture' often leads to messy code.

We love our diagrams. There is something deeply satisfying about drawing concentric circles, arrows pointing inward, and neat little boxes labeled "Entities," "Use Cases," and "Gateways." It feels like engineering. It feels like we have tamed the chaos of software development and trapped it within a perfect geometry.

But then Monday morning arrives. You need to add a single field to the user profile. And suddenly, that beautiful geometry becomes a prison. You find yourself editing a Request DTO, a Domain Entity, a Database Model, a Repository Interface, a Repository Implementation, a Use Case, a Controller, and a Presenter. You write four different mappers to move the same string from one box to another.

You have achieved "Clean Architecture." But your code is a mess.

We have adopted the aesthetics of architecture without understanding its purpose. We build layers of indirection not because the problem demands them, but because we believe that "good code" must look complicated.

The Promise of Swap-ability

The core sales pitch of Clean Architecture (and Hexagonal Architecture, and Onion Architecture) is independence. "The database is a detail," Uncle Bob tells us. "The web is a detail." By inverting dependencies, we ensure that our core business logic doesn't know or care whether it's running on a server, a lambda, or a command line.

The theoretical benefit is that you can swap out your database or framework without touching the business rules. It sounds amazing. Who wouldn't want that flexibility?

But let's be honest: When was the last time you swapped your database?

For 99% of projects, the answer is "never." You pick Postgres, and you stick with Postgres. Yet, you pay the "tax" of abstraction every single day. You write adapters and ports and interfaces for a migration that will never happen. You are buying insurance for a house that isn't built yet, in a city you might never live in.

The Cognitive Maze

Abstraction has a cost, and that cost is cognitive load. In a "clean" system, tracing the path of a single request requires jumping through five or six files. You lose the ability to simple "Go to Definition" and see what the code actually does. Instead, you land on an interface. Then you have to find the implementation. Then you find another interface.

We tell ourselves this is "separation of concerns." But often, it's justfragmentation of understanding.

True "Screaming Architecture" shouldn't scream "I read a book about patterns." It should scream the intent of the application. If I open your codebase and see `IUserRepository`, `UserRepositoryImpl`, `UserEntity`, `UserDTO`, and `UserMapper` before I see a single line of logic describing what a user actually does, the architecture is screaming noise, not signal.

The "Business Logic" Myth

The other premise is that we need to protect our precious "Business Logic" from the dirty outside world. But in modern web development, what is that logic, really?

For a vast majority of applications, the "business logic" is:

  • Validate the input.
  • Check if the user owns the record.
  • Save it to the database.
  • Return a 200 OK.

When you force this simple flow into a complex domain-centric architecture, you end up with "Anemic Domain Models"—entities that are just bags of getters and setters. You write "Use Cases" that do nothing but delegate to a repository. You have built a Rube Goldberg machine to transport JSON from an API endpoint to a SQL table.

Pragmatism Over Dogma

This isn't to say that layers are always bad. If you are building banking software where the interest calculation rules are complex, changing, and independent of the storage mechanism, then yes, isolate that logic. Use the pattern where it adds value.

But for the rest of us, we should embrace pragmatism.

Vertical Slices

Instead of horizontal layers (Controller, Service, Repository), organize by feature (CreateUser, UpdateOrder). Put everything needed for that feature in one folder. If "CreateUser" needs direct database access, give it direct database access.

Colocation

Keep things together that change together. If a DTO is only used by one controller, define it in the same file. Don't create a global `types` folder just because you feel like you should.

Complexity should be introduced only when the pain of simplicity becomes unbearable. Don't start with the solution to a problem you don't have yet.

"The goal of software architecture is to minimize the human effort required to build and maintain the system. If your architecture requires more effort for simple tasks, it is bad architecture, no matter how 'clean' it looks."