BoundariesCONTESTEDSIMPLIFIED

Onion Architecture

Concentric rings with the domain model at the centre. Nearly the same idea as hexagonal and Clean, drawn differently — and saying so is more useful than pretending they are three schools.

The requirement, the obvious build, and why it breaks

Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.

The question

Are hexagonal, Clean and onion three different architectures, or one idea with three diagrams?

The requirement

A team is choosing between three named styles for a new service. Two days have gone into the comparison and the discussion has become about which one is more correct.

The obvious build

Compare the three carefully, choose the best one, and follow it precisely. This is a foundational decision and it deserves the two days.

Why it breaks

The comparison has no answer, because the three are not competing claims. Onion puts the domain model at the centre and interfaces in the ring around it; Clean names four rings and calls the centre entities; hexagonal draws no rings at all and calls the boundary a port. The dependency rule is identical in all three (Choosing the Model).

How it breaks as requirements change
  • The comparison has no answer, because the three are not competing claims. Onion puts the domain model at the centre and interfaces in the ring around it; Clean names four rings and calls the centre entities; hexagonal draws no rings at all and calls the boundary a port. The dependency rule is identical in all three (Choosing the Model).
  • Two days spent on the naming means zero days spent on the question that has an answer: which of this system's dependencies actually vary, and how much genuine rule is there to protect (Requirements Before Design).
  • Picking a named style also imports its ceremony wholesale — including the parts that were incidental to the author's context — because "we are doing onion" makes it awkward to omit a ring (What a Framework Charges).
  • And it produces false confidence. A team that has chosen a style believes the structural question is settled, when nothing has yet been decided about where their own seams fall.
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

What limits the solution, and what must never stop being true

This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.

Constraints
  • The decision is being made before the service exists, so there is no evidence about which changes will arrive.
  • Whatever is chosen becomes the shape every subsequent file follows, so it is expensive to reverse — which is what is making the argument feel high-stakes (Design, Architecture and System Design).
  • The team has read three books that use different words for the same rings, which is most of why the discussion is stuck.
Invariants
  • Whichever diagram wins, the domain model must not import infrastructure. That is the only invariant any of them actually assert (Dependency Direction).
  • The rules must be exercisable without a database, or the choice of style has bought nothing measurable (Testing as Design Feedback).

Who owns what, and where the seams fall

Responsibilities decide boundaries; boundaries decide what an interface has to say.

Responsibilities
  • The domain model at the centre owns entities, value objects and the rules that bind them (Value Objects).
  • Domain services in the next ring own operations that do not belong to a single entity, expressed in domain terms (Domain Services).
  • Application services own use cases and the interfaces to the outside — in onion specifically, the repository interfaces live here rather than in the centre, which is the one real difference from Clean.
  • The outer ring owns infrastructure, UI and tests, and depends inward on everything else.
Boundaries
  • The only boundary all three styles agree on is between the rules and the outside. Every other line differs between the diagrams and none of the differences change what a change costs.
  • Where onion does differ usefully: putting repository interfaces in the application ring rather than with the entities is a defensible answer to a real question — is persistence a domain concern or an application one? Neither answer is wrong, and picking one takes a minute, not two days.
  • Because the rings are concentric, onion makes the "no skipping" question explicit: may transport call the domain model directly? Onion says the dependency is allowed, since all outer rings may depend inward. That is a genuine simplification over strict layering, which forbids it and thereby produces pass-through methods (Over-Decomposition).

The rings, and the one line that differs from Clean

Onion places the domain model at the centre, domain services around it, application services and the interfaces to the outside world around that, and infrastructure, UI and tests in the outer ring. Every dependency points inward and an outer ring may depend on any inner ring, not only the next one.

Against Clean, the substantive differences are two: repository interfaces live in the application ring rather than with the entities, and skipping rings is explicitly allowed. Against hexagonal, the difference is presentational — hexagonal draws one boundary with many ports on it, onion draws the same boundary as a circle with the rules inside. Nothing about what a change costs turns on any of this.

  • Repository interfaces in the application ring: a defensible answer to "is persistence a domain concern", not a better one (State Ownership).
  • Skipping allowed: this is the practical improvement over strict layering, because it removes forwarding methods.
  • Tests as an outer ring makes explicit what the other styles imply — tests are a client of the domain, not a layer above it.
  • The centre still depends on nothing. Every one of these styles is that sentence with different scenery.
Onion rings, with the direction that is the actual content
depends ondepends ondepends onmay skip — onion allows thisimplements its interfacesdepends on directlyUI / HTTPInfrastructure — DB, SDKsTests — also an outer ringApplication services + repository interfacesDomain services — operations spanning entitiesDomain model — entities, value objects, rules
UserLLMAgentToolDataDecisionHumanGuardrail

Three schools, or one rule and three vocabularies

The useful move is to stop comparing the diagrams and start comparing what each forbids. When you do that, the overlap is almost total, and the residue is a handful of naming choices that a team can settle in a meeting rather than a fortnight.

This matters practically because a team that believes it is choosing between schools will import a whole ceremony to be consistent with the one it picked. A team that knows it is choosing a vocabulary will take the dependency rule, take the adapters, and leave the parts that do not fit.

How the choice is usually framed, and how it is worth framing
Choosing a school
Q: "Should we do hexagonal, Clean or onion?"

  -> two days of reading
  -> a decision framed as correctness
  -> whole ceremony adopted for consistency
  -> ring count becomes a review argument
  -> still nothing decided about OUR seams:
       which dependencies vary?
       how much rule is there to protect?
       which changes do we actually get?
Choosing a rule, then a vocabulary
Q: "What must not import what, and where does
    the outside actually vary?"

  -> 30 min: domain/ imports nothing outward
  -> a CI check that asserts it
  -> ports only at seams with observed variation
  -> ring names picked to match team vocabulary
  -> written down in one paragraph, with a
     revisit trigger

  Renaming later: a day of mechanical refactor.

The dependency rule is what changes cost; the ring names are what changes conversations. Separating them turns an irreversible-feeling decision into two decisions, one of which is genuinely important and one of which is cheap to change later. The second framing also produces an enforceable artifact — a CI check — where the first produces an agreement that decays with the next hire (Revisit Triggers).

Five structures, five criteria, and where each row misleads

Widening the comparison past the three styles is more useful than sharpening it, because the real alternatives are not only different diagrams of the same rule — vertical slices and modular monolith cut the codebase on a different axis entirely.

Read the last column of every row. Each criterion is a genuine axis and each is also a place where the comparison flatters whichever structure the reader already prefers, so the caveat is part of the row rather than a footnote to the table.

  • No row has a winner, and the ones that look decisive are the ones where the criterion was chosen to suit an answer.
  • Hexagonal is on a different axis from the others: it is a decision about seams, not about how the whole codebase is laid out, so it composes with any of the remaining four.
  • The realistic combination in a healthy codebase is vertical slices inside modules, with ports at the two or three seams where the outside varies (Vertical Slices).
CriterionLayeredHexagonalCleanVertical sliceModular monolithWhere this row misleads
Ceremony and complexityLow — four folders, no mandated crossingsLow at one seam, high if applied uniformlyHighest — a model and mapper per crossingLow per slice; the shared-logic question is deferred, not solvedMedium — module boundaries plus internal structure inside eachCeremony is not the cost that matters; the cost is ceremony that buys nothing. Clean in a rules-heavy system is repaid, and slices in a system with heavy shared rules are not cheap at all — the cost has simply moved to duplication decisions (Duplicate Knowledge).
Change locality for a feature changePoor — every field crosses every folderUnaffected — ports address dependency variation, not feature shapePoor and multiplied by the mapping layersBest — the whole feature is one directoryGood within a module; poor for changes that span modulesOnly if changes are actually feature-shaped. A change to a rule that eight features share is worst under slices and best under a shared domain — and rules-sharing systems get that change constantly (Change Amplification).
Testability of the business rulesPoor by default — rules end up next to queriesExcellent — the core has no I/O by constructionExcellent, and the clearest of the fiveMixed — a slice usually tests through its handler and touches the databaseGood at the module boundary; module internals varyTestability of rules is not testability of the system. A design with pure, fast rule tests can still have every integration bug it started with, and slices that test through the handler catch a class of bug the pure tests never see (Where a Test Must Be Real).
Team fitFine for one team; a bottleneck when several teams share the service layerNeutral — a seam, not an organisational unitWorks when a whole org adopts it; painful when adoption is partialExcellent for parallel feature work with few merge conflictsBest when teams map to modules and each owns oneTeam fit is about ownership, and ownership is a decision you make, not a property of the structure. Any of these can be owned well or badly; the structure only makes bad ownership more or less visible (Code Ownership).
Domain complexity where it paysAny — it is the neutral default, which is also why it decaysIndependent of domain complexity; driven by dependency variation insteadHigh — needs real rules to hold, or the rings hold data classesLow to medium — thrives when features are independentMedium to high — needs enough domain to have distinct modules at allDomain complexity is not known up front, which is what makes this row the most misused in the table. Every one of these is defensible as a starting point, and the honest advice is to start with the least ceremony the system tolerates and add a boundary when a change proves you needed one (The Rule of Three).

How to build it

Most important first.

  • Decide the rule first and the diagram second. Write down "nothing in domain/ imports infrastructure/" and enforce it with a check; then choose whatever ring names your team finds clearest (Stable Dependencies).
  • Allow outer rings to skip inner ones. Forbidding it produces forwarding methods whose only purpose is to satisfy the drawing, and those are the clearest example in this domain of layers that separate nothing.
  • Choose where the repository interface lives on grounds of who owns the vocabulary, not on grounds of which book you follow. If the query is expressed in domain terms it belongs inward; if it is expressed in application terms it belongs in the application ring.
  • Adopt the smallest number of rings your system has distinct reasons to change for. Three is common; two is fine; four should have to justify itself (Module Granularity).
  • Write down which style you picked and why in one paragraph, so the next team does not spend the same two days (Architecture Decision Records).

What the next change costs

The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.

Cost of the next change
  • Changing a rule: one edit at the centre, one fast test, applies to every caller. Identical under all three styles — which is the point.
  • Adding a field: one edit per representation you chose to create. Onion as usually practised creates fewer than Clean, because it does not mandate a model per crossing, so a field change is typically three edits rather than five.
  • Changing style later: renaming rings is a day of mechanical refactoring, because the dependency rule is the same underneath. That is the strongest practical reason not to spend two days choosing — the decision is far more reversible than it feels (Reversible and Irreversible Decisions).
What the recommended approach costs
  • Saying "these are the same idea" costs the comfort of a named, authoritative standard, and some teams genuinely execute better with one.
  • Fewer rings means fewer forced representations and also fewer forced pauses to think about who owns what. The ceremony did buy something, and removing it removes that too.
  • Allowing outer rings to skip inner ones removes pass-through code and makes the dependency graph wider, which is harder to see at a glance (Fan-in and Fan-out).

What can go wrong

Failure modes
  • The rings are drawn and the ORM entity is used as the domain model, so the centre depends on infrastructure and the diagram is decorative (Invariant Leaks).
  • Ring discipline degrades into forwarding: an application service per entity, each with methods that call the repository and return, which is the anemic model with a ring diagram (The Anemic Domain Model).
  • The team treats ring count as a quality metric and adds one when the code feels disorganised, which reliably makes it worse (Over-Design and Under-Design).
  • The mitigation fails too: an import-direction check enforces the rings but says nothing about whether the centre holds any rules, so a codebase can be perfectly compliant and completely anemic.
Dependencies, and their direction
  • All rings depend inward, transitively, and the centre depends on nothing but the language.
  • Onion permits an outer ring to depend on any inner ring, not just the adjacent one — a deliberate relaxation of strict layering, and the source of its main practical advantage.
  • The dependency on the *naming* is the one nobody counts: once a team says "onion", every review argument acquires a textual authority, and disagreeing with the diagram becomes disagreeing with a book (Design Review).
Misreads
  • "So the style does not matter at all." The dependency rule matters a great deal; the choice among the three drawings of it does not. Those are different claims (Dependency Direction).
  • "They are literally identical." They are not — repository interface placement, ring count and whether skipping is allowed genuinely differ. The claim is that none of those differences move change cost much, which is a smaller and more defensible statement.
  • "Then pick any of them at random." Pick the one whose vocabulary your team already uses, because the vocabulary is the part that pays off in review, and then stop discussing it (Ubiquitous Language).
  • "Onion is layered architecture with a nicer picture." Strict layering forbids skipping and permits the domain to depend on data access; onion inverts the second and relaxes the first. Those are the two changes that matter (Package by Layer).
Smells this explains
  • divergent-change

Testing it, and how it ages

What to test, and at which boundary
  • The centre must be testable with construction and assertion only. This test is identical across the three styles and is the only one that distinguishes a real boundary from a drawn one (What a Unit Is).
  • Assert the import direction in CI. It is the cheapest structural test available and it is what keeps the rings from decaying into folders.
  • Test the application ring against in-memory repositories, and the outer ring against real infrastructure (Where a Test Must Be Real).
How this design ages
  • What survives in every codebase that adopted one of these is the dependency rule and the adapters. What erodes is the ring count, because ceremony without a felt benefit does not survive delivery pressure.
  • Teams converge over a few years on a pragmatic hybrid — inward dependency, one model at the external contract, direct calls elsewhere — regardless of which style they started from. Recognising that end state early saves the intermediate years.
  • The style stops mattering entirely once the codebase is split into modules that each make their own choice, which is the transition into a modular monolith (The Modular Monolith).

Where this applies

This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.

  • CONTESTEDThe strongest opposing case: the differences are not cosmetic, and treating them as such is how teams end up with a mushy hybrid that has neither style's discipline. Clean's mandatory crossing model genuinely prevents ORM leakage that onion permits, and a named standard with a book behind it is enforceable by a new hire in a way that "our pragmatic hybrid" is not. That argument is right that the discipline is the valuable part; it is wrong that the ring count is where the discipline lives — enforcement lives in a CI check and a review habit, and those work identically under any of the three names.
  • SIMPLIFIEDPresenting the three as one idea deliberately omits their differing treatments of presenters, of where validation belongs, and of whether the application ring may hold state. Those distinctions are real in the original texts and are left out because they do not change what the next change costs, which is the criterion this domain uses.

Where the depth lives

This domain teaches the codebase-level structure and hands the rest off.

Domains that do not exist yet
  • System Design — the same "which of these is correct" argument recurs one grain up, between event-driven and request-response shapes, and is resolved the same way: by naming the changes you expect rather than the style you admire.