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.
Are hexagonal, Clean and onion three different architectures, or one idea with three diagrams?
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.
Compare the three carefully, choose the best one, and follow it precisely. This is a foundational decision and it deserves the two days.
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).
- 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.
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.
- 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.
- 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.
- 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.
- 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.
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.
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?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).
| Criterion | Layered | Hexagonal | Clean | Vertical slice | Modular monolith | Where this row misleads |
|---|---|---|---|---|---|---|
| Ceremony and complexity | Low — four folders, no mandated crossings | Low at one seam, high if applied uniformly | Highest — a model and mapper per crossing | Low per slice; the shared-logic question is deferred, not solved | Medium — module boundaries plus internal structure inside each | Ceremony 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 change | Poor — every field crosses every folder | Unaffected — ports address dependency variation, not feature shape | Poor and multiplied by the mapping layers | Best — the whole feature is one directory | Good within a module; poor for changes that span modules | Only 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 rules | Poor by default — rules end up next to queries | Excellent — the core has no I/O by construction | Excellent, and the clearest of the five | Mixed — a slice usually tests through its handler and touches the database | Good at the module boundary; module internals vary | Testability 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 fit | Fine for one team; a bottleneck when several teams share the service layer | Neutral — a seam, not an organisational unit | Works when a whole org adopts it; painful when adoption is partial | Excellent for parallel feature work with few merge conflicts | Best when teams map to modules and each owns one | Team 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 pays | Any — it is the neutral default, which is also why it decays | Independent of domain complexity; driven by dependency variation instead | High — needs real rules to hold, or the rings hold data classes | Low to medium — thrives when features are independent | Medium to high — needs enough domain to have distinct modules at all | Domain 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.
- 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).
- 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
- 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.
- 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).
- "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).
- divergent-change
Testing it, and how it ages
- 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).
- 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.
- — 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.