Open almost any codebase that advertises "Clean Architecture" and you will find the same thing. Four project folders named Domain, Application, Infrastructure, and Presentation. A User entity, a UserDto, a UserModel, a UserResponse, and four mapper functions that copy the same five fields between them. A GetUserByIdUseCase whose entire body is one line that calls a repository. And a team that half-believes this is what good software is supposed to look like, because a respected book told them so.
The book is right about one thing and quiet about the cost of the rest. Clean Architecture, the version Robert C. Martin published in 2012, is not four rings. It is a single rule with a diagram wrapped around it. The rule is worth internalizing for the rest of your career. The rings are a suggestion that turns into a tax the moment you stop asking whether you need them. This post is about telling the two apart.
The whole thing is one rule
Strip away the concentric-circle diagram and Clean Architecture reduces to what Martin calls the Dependency Rule: source code dependencies point only inward. Nothing in an inner circle knows anything about an outer circle. Your business logic must not import your web framework, your ORM, your database driver, or your JSON serializer. Not the types, not the annotations, not the names.
That is the entire idea. Everything else in the diagram, the four named layers, the entities-versus-use-cases split, the ports and adapters, is a specific way to obey that one rule. Jeffrey Palermo said the same thing four years earlier when he described Onion Architecture: "all coupling is toward the center." Alistair Cockburn said it before both of them with Hexagonal. Three diagrams, three vocabularies, one rule. If you have read this newsletter's hexagonal post, this will feel familiar, and that is the point: these are not competing architectures. They are the same rule drawn differently.
Why does the rule matter? Because it decides what your code is allowed to depend on, and dependencies are the thing that make software hard to change. When your order-pricing logic imports jakarta.persistence.Entity, you cannot test pricing without a database, cannot reuse it in a batch job that has no web layer, and cannot swap Postgres for anything without touching the rule that computes discounts. The Dependency Rule is a firewall. Business rules on one side, mechanisms on the other, and the arrows only cross in one direction.
Here is the rule in its smallest honest form. A domain that depends on an interface it owns:
# domain layer - imports nothing from infrastructure
class Order:
def __init__(self, lines: list[Line]):
self.lines = lines
def total(self) -> Money:
return sum((l.subtotal() for l in self.lines), Money.zero())
class OrderRepository(Protocol): # the port, owned by the domain
def save(self, order: Order) -> None: ...
def by_id(self, id: OrderId) -> Order | None: ...
# infrastructure layer - imports the domain, implements its port
class PostgresOrderRepository(OrderRepository):
def __init__(self, pool: ConnectionPool):
self._pool = pool
def save(self, order: Order) -> None:
# SQL lives here, and only here
...
The domain declares OrderRepository and never learns that Postgres exists. Infrastructure reaches inward to implement it. The arrow crosses inward. That is Clean Architecture. You could stop reading here and you would have the 90% that matters.
The rings are where the cost hides
The famous diagram has four rings: Entities, Use Cases, Interface Adapters, Frameworks and Drivers. Martin is careful in the original article to say the number is not sacred, that "there's no rule that says you must always have just these four." Almost nobody quotes that sentence. What propagates instead is a project template with four fixed folders and an unspoken expectation that data crosses every boundary through a dedicated object, so nothing inner ever touches an outer type.
Follow that expectation literally and you get the mapping tax. The same User becomes a chain of near-identical shapes: an HTTP request body, an application-layer command, a domain entity, a persistence model, and an HTTP response. Each boundary gets a mapper. Add one field, phoneNumber, and you now edit the request DTO, the command, the entity, the persistence model, a database migration, the response DTO, and every mapper in between. That is the real complaint behind the "Clean Architecture is a maintenance nightmare" posts, and it is a fair complaint. The Three Dots Labs team, who are broadly sympathetic to the style, put the honest test plainly: a layer earns its place only when it holds logic that would otherwise leak. A mapper between two structurally identical objects holds no logic. It is ceremony.
The one-line use case is the other tell. When your GetProductByIdUseCase looks like this:
class GetProductByIdUseCase:
def __init__(self, repo: ProductRepository):
self._repo = repo
def execute(self, id: ProductId) -> Product | None:
return self._repo.by_id(id) # that is the whole use case
you have built a class, an interface, a test double, and a wiring entry so that a controller can avoid calling a repository directly. There is no orchestration here, no business rule, no transaction boundary, nothing the layer is supposed to protect. You paid for a boundary and bought a redirect. Martin's use-case layer is meant for interactors that orchestrate entities to accomplish something ("process a payment", "cancel a subscription"). A pass-through read is not that. Forcing it into the shape anyway is how Clean Architecture gets its reputation for turning a CRUD screen into fourteen files.
None of this is Martin being wrong. It is teams reading a diagram as a mandate instead of a menu. The rings answer the question "how might you organize code that obeys the Dependency Rule?" They were never meant to answer "how many objects must this field pass through?" with "all of them, always."
Keep the rule, price the rings
The useful reframing is to treat the Dependency Rule as non-negotiable and every ring as a purchase you justify.
The rule costs almost nothing. Pointing your dependencies inward is mostly a matter of where you declare interfaces and which module owns them. You can obey it in a single project, in a single folder, with zero mappers, by doing one thing: let the domain define the interfaces it needs and keep framework types out of it. A flat service that takes a repository interface and returns a domain object already satisfies the rule. You do not need four assemblies to get the testability and swappability people actually want.
The rings cost real money, and the price is worth paying exactly when the shapes on either side of a boundary genuinely diverge. A separate persistence model earns its keep when your table layout does not match your domain, which happens the moment you have a rich aggregate stored across three tables, or a legacy schema you cannot change. A separate response DTO earns its keep when your API contract must stay stable while your domain evolves, or when the API exposes a different shape than you store (hiding internal fields, flattening a graph, versioning). A dedicated use-case interactor earns its keep when it coordinates multiple entities inside a transaction, enforces an invariant that spans them, or is a real seam you mock in tests. In each case the layer holds something. When it holds nothing, delete it and let the caller talk to the next thing directly. The Dependency Rule survives; only the ceremony dies.
A practical heuristic: map at the edges where shapes actually differ, and let them be the same object everywhere else. If your API request, your domain object, and your row are structurally identical five-field bags with no behavior, they are one type, and a "Clean" split into three is negative value. If your domain aggregate is nothing like your wire format, map, and be glad you have the seam. The mistake is not having mappers. The mistake is having them by policy instead of by need.
This is also why Clean Architecture pays off unevenly. It shines on long-lived systems with complex behavior and multiple delivery mechanisms, which is exactly the case Palermo carved out in 2008 when he warned Onion Architecture "is not appropriate for small websites." A four-week internal CRUD tool has no complex domain to protect, no second delivery mechanism, and no plausible database swap. Applying the full ring structure there is paying insurance premiums on a rental car you return on Friday.
Yes, but the boilerplate is what makes it safe
The strongest defense of the full ceremony goes like this: the mappers and the redundant models are not waste, they are a firewall against coupling you cannot see yet. The day your API needs to keep a field your domain wants to rename, you will be glad the DTO already exists. The day someone tries to leak a persistence annotation into the domain, the separate model stops them. Boilerplate is cheap; a domain silently welded to your ORM is expensive. Collapse the layers and you have optimized for the easy 80% of changes at the cost of the hard 20%.
This is a real argument and it is right about the risk. Coupling does creep in exactly where you removed the boundary, and retrofitting a seam under pressure is worse than having kept it. But the argument proves less than it claims. It justifies keeping the seams that protect divergence, the ones where the shapes plausibly drift apart, not every seam by default. You do not need a distinct model between two things that are provably identical and have no reason to differ, because there is no coupling to prevent. And when you do discover a reason to split, introducing a DTO at one boundary is a local, mechanical refactor, not an architectural rewrite, precisely because you kept the Dependency Rule the whole time. The rule is what makes the seams cheap to add later. That is the thing to protect. The preemptive mappers are insurance you can buy the day you see the risk, on the boundary that actually has it, rather than on all of them up front.
What to do Monday
Go find the most "Clean" module in your codebase and count the objects one field passes through on its way from HTTP to the database. If the answer is five and none of those objects has behavior or a divergent shape, you have found ceremony, not architecture. Collapse the identical ones into a single type. You will delete mappers, tests for mappers, and wiring, and nothing a user cares about will change.
Then check the one thing that actually matters: grep your domain layer for imports of your web framework, your ORM, and your database driver. If any show up, that is your real architecture problem, and it will still be there after all the folders are perfectly named. Fix the direction of the arrows first. The rings are negotiable. The rule is not.

