← All posts

Opinion Architecture

"Clean Architecture" Is Overkill for 80% of Apps

Clean Architecture burdens simple apps with ceremony they don't need. For 80% of apps, plain modular code wins.

Developer beside overly complex whiteboard diagram, pointing, with a simple app on a laptop showing the contrast

If I see one more simple CRUD app split into entities, use cases, repositories, gateways, presenters, and six layers of interfaces before it even talks to a database, I am going to scream. Clean Architecture, as popularized by Robert Martin, is a genuinely useful mental model for structuring code in complex domains. But it has escaped the intended context and become a default dogma. Most apps do not need it. For 80 percent of what gets built, Clean Architecture is not engineering discipline—it is sheer ceremony that adds indirection, file count, and cognitive load without delivering the benefits that justify the cost.

The gap between the ideal and the actual app

Clean Architecture describes a system where business rules are independent of frameworks, databases, and user interfaces. The core domain sits at the center, surrounded by adapter layers that translate external concerns into the domain language. In a genuinely complex system—think an insurance underwriting engine with tangled business rules, or a trading platform with regulatory constraints—that independence pays off. You can write fast unit tests against the domain, swap out infrastructure, and reason about the rules in isolation.

The problem is that most applications are not that. Most apps take data from a database, apply a handful of straightforward rules—if any—and return it to a UI. The "domain" is thin, sometimes nonexistent. When you apply Clean Architecture to a thin domain, you end up with six files and three layers of indirection to move data from a controller to a database and back. The architecture is working; the functional value being delivered is not meaningfully improved.

The concrete cost of putting Clean Architecture where it does not belong

Adding Clean Architecture to a simple application is not free. The costs are real and compound over time:

  • Every small change touches many files. Adding a field to a form can require changes to the entity, the use case input/output ports, the controller, the presenter, and the repository interface. That is friction without value.
  • Onboarding gets harder. A new developer on a simple app should be able to understand the flow in minutes. With Clean Architecture, they must learn the project's specific adapter wiring, dependency injection setup, and interface conventions before they can make a safe change.
  • Testing overhead grows. The architecture encourages mocking every dependency at every layer boundary. For thin business logic, those tests become noise: they validate wiring, not behavior, and they slow down refactoring.
  • File count explodes. A single create-user operation might live across use case, request DTO, response DTO, controller, presenter, interface, and implementation. None of that is necessary when the operation is essentially a database insert with basic validation.
  • The team spends more time maintaining the architecture than delivering value. Conversations shift from "what does the user need?" to "which layer should this go in?" The architecture becomes the point, not the product.

I have seen teams with four people spend more time debating interface placement than they spent on the feature logic itself. At that point, Clean Architecture is not enabling productivity—it is consuming it.

The counterargument: “but when it gets complex, you will be glad you had it”

The standard defense of early Clean Architecture is that you should design for complexity before it arrives, because adding it later is hard. This argument is not entirely wrong: a system that evolves into genuine complexity without architectural boundaries will be painful to refactor.

But the defense conflates "complex" with "complicated." Many apps grow in scale—more users, more data—without developing a richer domain. Scale problems are solved with caching, queueing, database optimization, and eventually service decomposition. None of that is helped by a hexagonal domain layer. The architecture that tries to solve scale through layers is solving the wrong problem.

For apps that genuinely do develop rich business rules over time, the more pragmatic path is not to build Clean Architecture from day one on speculation. It is to extract a domain core when the domain actually gets rich enough to merit it. You can start with a simple layered model—controllers, service objects, data access—and keep the business logic cleanly separated from infrastructure without the full ceremony. If the rules become numerous and intertwined enough that testing them independently becomes worth the cost, then carve out a domain layer and add proper ports and adapters. You will do a better job at that point because you will actually understand the domain.

What simple architecture looks like without being sloppy

Rejecting Clean Architecture for simple apps is not a vote for chaos. It is a vote for architecture that matches the problem's complexity. For most applications, that means:

  • Group code by feature or module, not by layer. A "users" folder that contains the handler, validation, service logic, and data access for user operations is easier to navigate than scattering those concerns across a dozen layer-based packages.
  • Use a single service layer that contains business logic, without splitting it into use case classes unless there are genuinely distinct workflows that need independent orchestration.
  • Access the database directly from that service layer. Abstracting the database behind a repository interface in a simple app is indirection that rarely pays off. If you switch databases later, the repository interface will not save you anywhere near as much as it cost.
  • Keep testing practical. Write integration tests that exercise the database rather than mocking it away. Write narrower unit tests only for the small subset of logic that is complex enough to warrant isolation.
  • Be disciplined about modularity within that simpler structure. Simple architecture still needs clear boundaries between modules to avoid a spaghetti mess. The key is boundaries where they matter, not uniform layering everywhere.

This kind of architecture is not "Clean Architecture lite." It is just good, focused code that acknowledges the domain is thin and prioritizes developer speed over architectural purity.

Where I land

I respect Clean Architecture. I have used it on systems where the domain was genuinely thick with business rules that changed frequently and needed independent testability. In that context, the architecture paid for itself. The layers made refactoring safe, and the tests ran fast without infrastructure. I was glad I had it.

But those systems are the exception. For the 80 percent of apps that are essentially CRUD with light rules, a dash of workflow, and a clear data-to-screen pipeline, Clean Architecture is overkill. It increases cost, drags velocity, and adds ceremony that the problem does not demand.

Before you add ports, adapters, use cases, and dependency rules to your next project, ask honestly: is the business logic genuinely complex enough to make all that indirection worthwhile? If the answer is not a clear yes, choose a simpler architecture that matches the job. You will ship faster, onboard more easily, and spend your cognitive energy on the product, not on the layers.

The most professional architecture choice is the one that fits the problem, not the one that looks most impressive on a whiteboard.