Onion Architecture & Clean vs Hexagonal vs Onion
Jeffrey Palermo's Onion Architecture layer by layer, then a clear side-by-side comparison of Clean, Hexagonal and Onion, showing what's really different and what is the same idea drawn three ways.
The big idea
Peel an onion: the outer skin can be thrown away and replaced, but the heart in the middle is what the whole onion grows around.
Onion Architecture (Jeffrey Palermo, 2008) organises an application as rings around the domain model. Outer rings depend on inner rings, never the other way round. Infrastructure (databases, UI, frameworks) is the outer skin.
Onion architecture: rings around the domain model
The rings
| Ring (inside β out) | Contains | Example |
|---|---|---|
| π‘ Domain model | Entities, value objects, business rules | Order, Money, Email |
| π Domain services | Business logic that doesn't belong to one entity; repository interfaces | PricingService, IOrderRepository |
| π΅ Application services | Use cases / workflows orchestrating the domain | CheckoutService.placeOrder() |
| π£ Outer ring: infrastructure, UI, tests | Implementations of the interfaces, web framework, DB, external APIs | SqlOrderRepository, controllers, EF Core / Prisma |
The key move: interfaces live in the inside
Just like the other two styles, the repository interface is declared in an inner ring (domain services), and implemented in the outer ring. Palermo's own summary of the principles:
- The application is built around an independent object model.
- Inner layers define interfaces; outer layers implement them.
- The direction of coupling is toward the centre.
- All application core code can be compiled and run separately from infrastructure.
// Onion Architecture grew up in the .NET world, so here's the classic C# flavour
namespace Shop.Domain.Services { public interface IOrderRepository { Task Save(Order order); } }
namespace Shop.Application { public class CheckoutService(IOrderRepository orders) { /* β¦ */ } }
namespace Shop.Infrastructure.Sql { public class SqlOrderRepository : IOrderRepository { /* EF Core */ } }
Shop.sln
βββ Shop.Domain (no project references)
βββ Shop.Application β references Domain
βββ Shop.Infrastructure β references Application, Domain
βββ Shop.Api β references everything (composition root)
π‘ Separate projects/packages per ring let the compiler enforce the dependency rule:
Shop.Domainliterally cannot import Entity Framework. In TypeScript you can get the same with separate packages, or with lint rules likeeslint-plugin-boundaries/dependency-cruiser.
Clean vs Hexagonal vs Onion β
The honest answer: they're the same core idea
All three were created to fix the same problem with classic layered architecture: business logic depending on the database and frameworks. All three share the same rule:
π― The business logic is at the centre and depends on nothing. Infrastructure is at the edge and depends on the centre, through interfaces owned by the centre.
The same idea drawn three ways
What each one emphasises
| Hexagonal (2005) | Onion (2008) | Clean (2012) | |
|---|---|---|---|
| Author | Alistair Cockburn | Jeffrey Palermo | Robert C. Martin |
| Main picture | Hexagon with ports on its sides | Concentric rings | Concentric circles |
| Main emphasis | Symmetry of inputs and outputs: every outside actor (UI, test, DB) is just an adapter on a port | Layers inside the core: domain model β domain services β application services | The Dependency Rule + named layers, use cases as first-class objects, crossing boundaries with DTOs |
| Inside the core | Not prescribed ("the application") | Domain model, domain services, application services | Entities, use cases |
| Key vocabulary | Ports, adapters, driving, driven | Domain model, rings, infrastructure | Entities, use cases, interface adapters, presenters |
| Testing story | Tests are just another driving adapter | Core runs without infrastructure | Use cases testable without UI, DB, web |
Mapping the terms
| Concept | Hexagonal | Onion | Clean |
|---|---|---|---|
| Business objects and rules | (inside the app) | Domain model | Entities |
| Application workflows | (inside the app) | Application services | Use cases (interactors) |
| Interface the core needs | Driven (secondary) port | Repository / service interface in domain services | Output port / gateway interface |
| Interface the core offers | Driving (primary) port | Application service interface | Input port / boundary |
| Web controller | Driving adapter | Outer ring (UI) | Interface adapter (controller) |
| Database code | Driven adapter | Outer ring (infrastructure) | Interface adapter (gateway) + frameworks & drivers |
Which should I pick?
It matters much less than people think. Pick the vocabulary your team understands, and enforce the one rule they share. In practice, most teams blend them:
- Hexagonal's names for the boundary: ports and adapters, driving and driven.
- Onion's / DDD's layering inside the core: domain model, domain services, application services.
- Clean's one-class-per-use case and the "screaming" folder structure.
src/ordering/
βββ domain/ # entities, value objects, domain services (Onion / DDD / Clean entities)
βββ application/ # use cases + ports (Clean use cases, Hexagonal ports)
βββ adapters/ # driving: http, cli Β· driven: postgres, stripe (Hexagonal adapters)
How these relate to Layered architecture
The single difference is where the database sits: at the bottom that everything depends on, or at the edge where it depends on the domain.
Key takeaways
- Onion: rings around the domain model (domain services, then application services, then infrastructure/UI); inner rings define interfaces, outer rings implement them.
- Hexagonal, Onion and Clean share one rule: business logic in the centre, depending on nothing; infrastructure at the edge, implementing interfaces owned by the centre.
- They differ in emphasis and vocabulary: Hexagonal β ports/adapters and input/output symmetry; Onion β layers inside the core; Clean β the Dependency Rule, use cases and boundaries.
- Blend them freely; enforce the dependency direction with project structure or lint rules.