System Design

Clean Architecture and Onion Architecture: The Same Idea, Drawn Differently

I've reviewed a lot of codebases over the years where someone had clearly read Uncle Bob's Clean Architecture book, gone back to their team, and said "we'...

18 Apr 2026

Clean Architecture and Onion Architecture: The Same Idea, Drawn Differently

I've reviewed a lot of codebases over the years where someone had clearly read Uncle Bob's Clean Architecture book, gone back to their team, and said "we're doing this now." Six months later, the codebase had four nested folders per feature, dozens of interface files that wrapped a single method, and developers who had stopped caring because every new endpoint required touching eight files. The architecture had been followed. The goals behind it had been missed entirely.

Clean Architecture and Onion Architecture are not the same thing, but they solve the same problem. Understanding what that problem actually is — not the diagrams, the problem — is what separates teams that use these patterns well from teams that just use them.

The problem both architectures are solving

Every application eventually develops the same disease: business logic that depends on infrastructure. A service class that imports directly from an ORM. A domain model that knows what an HTTP request looks like. Validation rules scattered across controllers. The symptom isn't messy code. The symptom is that changing your database means rewriting your business rules, or switching from REST to gRPC means the core of your system has to change.

Both Clean Architecture (Robert Martin, 2017) and Onion Architecture (Jeffrey Palermo, 2008) are answers to this. The core rule in both is the same: dependencies flow inward. Outer layers can depend on inner layers. Inner layers cannot depend on outer layers. The business logic at the center knows nothing about frameworks, databases, or delivery mechanisms.

That single rule, if you actually follow it, solves the coupling problem. Everything else in both architectures is elaboration.

What Clean Architecture says

Martin's version organizes code into four concentric layers: Entities, Use Cases, Interface Adapters, and Frameworks & Drivers.

Entities are the enterprise-wide business rules. These don't know about your app. They're the pure domain objects — an Order, a User, a Transaction. If you deleted your entire application and rewrote it, the entities would survive unchanged.

Use Cases encode application-specific business rules. "When a user places an order, reserve the inventory and send a confirmation." This layer orchestrates entities but still doesn't know whether the confirmation goes via email or SMS. It doesn't know if inventory lives in Postgres or Redis. It calls abstractions — interfaces — and those interfaces are implemented somewhere further out.

Interface Adapters convert data between the use case format and the outside world. Controllers, presenters, gateways, repository implementations. This is where you translate an HTTP request into a use case input, and a use case result into an HTTP response.

Frameworks & Drivers is the outermost ring. Express, Django, SQLAlchemy, your test database, your message queue client. All the volatile stuff that you swap out without touching anything inward.

The clean architecture diagram looks like a bullseye. The dependency rule goes: outer rings can call inward, never the reverse.

What Onion Architecture says

Palermo drew the same idea differently. He named his layers: Domain Model, Domain Services, Application Services, and an outer Infrastructure layer.

Domain Model is the center — pure domain objects with behavior, not just data bags.

Domain Services contain domain logic that doesn't naturally fit inside a single entity. Things like a PricingService that needs to coordinate between a Product, a Customer, and a Discount.

Application Services orchestrate use cases. Very close to Martin's Use Case layer. They call domain services, read and write through repository interfaces, and handle cross-cutting concerns like transactions.

Infrastructure is everything at the edges: database implementations, file system access, external API clients, message broker adapters.

If you squint, Domain Model ≈ Entities, Domain Services ≈ part of Use Cases, Application Services ≈ Use Cases + Interface Adapters, Infrastructure ≈ Frameworks & Drivers. The names are different. The idea is not.

Where people go wrong

The most common failure mode I see is treating the architecture as a folder structure rather than a dependency rule. Teams create /domain, /application, /infrastructure directories, move files into them, and declare victory. But if the Order class in /domain imports from typeorm, you've violated the core rule regardless of what folder it lives in.

The second failure is interface proliferation. The dependency inversion principle — inner layers define interfaces, outer layers implement them — is real and important. But it doesn't mean every class needs an interface. It means your use case layer shouldn't import UserRepository from a specific database package. It should import IUserRepository and not know what implements it. One interface per repository is fine. One interface per service class is usually a smell.

The third failure is the anemic domain model. When you have an Order entity that's just a bag of fields, and all the logic lives in an OrderService, you've shifted complexity outward without actually containing it. The domain model layer exists to hold behavior, not just shape.

When these architectures pay off

For a CRUD app serving a mobile frontend, Clean Architecture is probably overkill. You're adding indirection without much payoff because your business logic is thin.

Where it pays off: applications with complex domain rules, multiple delivery mechanisms (REST + gRPC + async workers), or where you genuinely need to swap infrastructure over time. I've worked on a system where we migrated from one database to another with no changes to the business logic layer, because the repository interfaces had been held to properly. That's not theoretical — it actually happens, and it's only possible when the dependency rule has been followed consistently from the beginning.

The test argument is also real. When your use cases have no framework dependencies, you can test them with plain unit tests. No mocks of HTTP, no in-memory databases, just inputs and outputs. The test suite for the business logic becomes fast, deterministic, and readable. That's worth a lot.

Choosing between them

Practically speaking, Clean Architecture gives you more explicit layer names and a cleaner separation of interface adapters from use cases. Onion Architecture tends to emphasize a richer domain model more strongly, borrowing from DDD vocabulary. If your team is already thinking in DDD terms — aggregates, domain events, bounded contexts — Onion Architecture's language may fit better. If you're coming from a layered-architecture background and want a clearer path there, Clean Architecture's vocabulary might land better.

Neither is wrong. They're both diagrams of the same dependency rule.

The question worth spending time on isn't which one to pick. It's how to enforce the dependency rule in your actual codebase, because no naming convention enforces it for you. Some teams use module boundary tools (like dependency-cruiser for Node.js) to fail the build when an inner layer imports from an outer one. That's a more reliable guard than code reviews.

If you're thinking through the boundaries of your current system, I run mentorship sessions where we work through exactly this kind of architectural decision. And if you want a weekly take on system design and senior engineering, the Monday BY Gazar newsletter covers it every week.