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.

Intermediate⏱ 5 min readLesson 6 of 13#architecture#onion#clean-architecture#hexagonal#comparison

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 modelOnion architecture: rings around the domain model

The rings

Ring (inside β†’ out)ContainsExample
🟑 Domain modelEntities, value objects, business rulesOrder, Money, Email
🟠 Domain servicesBusiness logic that doesn't belong to one entity; repository interfacesPricingService, IOrderRepository
πŸ”΅ Application servicesUse cases / workflows orchestrating the domainCheckoutService.placeOrder()
🟣 Outer ring: infrastructure, UI, testsImplementations of the interfaces, web framework, DB, external APIsSqlOrderRepository, controllers, EF Core / Prisma
Drawing diagram…

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:

  1. The application is built around an independent object model.
  2. Inner layers define interfaces; outer layers implement them.
  3. The direction of coupling is toward the centre.
  4. 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.Domain literally cannot import Entity Framework. In TypeScript you can get the same with separate packages, or with lint rules like eslint-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 waysThe same idea drawn three ways

What each one emphasises

Hexagonal (2005)Onion (2008)Clean (2012)
AuthorAlistair CockburnJeffrey PalermoRobert C. Martin
Main pictureHexagon with ports on its sidesConcentric ringsConcentric circles
Main emphasisSymmetry of inputs and outputs: every outside actor (UI, test, DB) is just an adapter on a portLayers inside the core: domain model β†’ domain services β†’ application servicesThe Dependency Rule + named layers, use cases as first-class objects, crossing boundaries with DTOs
Inside the coreNot prescribed ("the application")Domain model, domain services, application servicesEntities, use cases
Key vocabularyPorts, adapters, driving, drivenDomain model, rings, infrastructureEntities, use cases, interface adapters, presenters
Testing storyTests are just another driving adapterCore runs without infrastructureUse cases testable without UI, DB, web

Mapping the terms

ConceptHexagonalOnionClean
Business objects and rules(inside the app)Domain modelEntities
Application workflows(inside the app)Application servicesUse cases (interactors)
Interface the core needsDriven (secondary) portRepository / service interface in domain servicesOutput port / gateway interface
Interface the core offersDriving (primary) portApplication service interfaceInput port / boundary
Web controllerDriving adapterOuter ring (UI)Interface adapter (controller)
Database codeDriven adapterOuter ring (infrastructure)Interface adapter (gateway) + frameworks & drivers
Drawing diagram…

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

Drawing diagram…

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.