Comparisons
Pairs that get conflated in real conversations and in real pull requests — coupling and cohesion, abstraction and indirection, refactoring and rewriting, debt and mess. Neither column wins; what decides is the requirement. Each record leads with the confusion, because the confusion is the reason the record exists.
Inheritance vs Composition
"Favour composition over inheritance" has calcified into "inheritance is bad", which is not what the advice says and would be a strange thing to believe about a feature at the centre of several successful languages. The real argument is narrower and sharper. Inheritance couples the subclass to the *implementation* of the base, not just its interface: a protected field, a method the base calls internally, an assumption about the order in which template hooks run — all of it becomes contract, none of it appears in a signature, and the base class author cannot change any of it without breaking subclasses they have never seen. That is the fragile-base-class problem, and it is why a deep hierarchy is expensive to change. The second argument is combinatorial: inheritance gives you one axis of variation, so two independent axes produce a cross-product of classes and three produce a mess. Composition converts both problems into ordinary, visible dependencies. But it costs something too, and the advocacy usually skips it: delegation is boilerplate, the object graph is wider, and a hierarchy that genuinely is closed and substitutable expresses that fact better than a bag of injected collaborators. The version worth keeping is: use inheritance for substitutable variants of a type you own; use composition for reuse.
When there is a genuine is-a relationship with substitutability, when the base type is a stable, closed abstraction you control, and when the variation is in behaviour that the base type explicitly left open — a sealed hierarchy of expression nodes, a framework extension point, a set of variants you enumerate.
When you want to reuse behaviour without claiming a type relationship, when the parts vary independently, or when the same object needs to combine several capabilities. It is also the choice that keeps working when the number of varying dimensions grows past one.
| Dimension | Inheritance — a subtype extends and can substitute for a base type | Composition — an object holds collaborators and delegates to them |
|---|---|---|
| Relationship expressed | is-a, with substitutability as an obligation | has-a, or uses-a |
| Coupled to | The base class implementation, including its internals | The collaborator interface only |
| Bound when | Compile time, once, permanently | Runtime, and it can change per instance |
| Axes of variation | One; two axes give you a cross-product of classes | As many as you have collaborators |
| Reuse mechanism | Implicit — inherited members appear without being asked for | Explicit — you write the delegation you want |
| Failure mode | Fragile base class; a subtype that quietly breaks callers | Delegation boilerplate and a wider object graph |
| Testability | The subclass drags the base's dependencies into every test | Substitute the collaborator; nothing else moves |
| Where it is the right answer | Closed variant sets, framework hooks, sealed hierarchies | Reuse across unrelated types, and anything varying on more than one axis |
The same question, five structures
Layered, hexagonal, clean, vertical slice and modular monolith — compared without naming a winner, and with the block that says where the comparison stops being true.
A tidy table implies an equivalence that does not exist. These are not five points on one axis: layered, hexagonal and clean are statements about dependency direction; vertical slice is a statement about directory grouping; modular monolith is a statement about deployment and module visibility. Most real systems combine several. The where this comparison misleads block on every row is the part worth reading, and it is the reason this page names no winner — none of these is mandatory, and a team that adopts one because a diagram was pretty has skipped the only question that decides it.
These are not five points on one axis. Layered, hexagonal and clean are all statements about *dependency direction*; vertical slice is a statement about *directory grouping*; and modular monolith is a statement about *deployment and module visibility*. You can — and most real systems do — combine several of them: a modular monolith whose modules are vertical slices, each with a hexagonal boundary at its edges. Comparing them as alternatives is the single most common way this table is misread.
The word complexity is doing two jobs here. Layered and vertical slice are cheap to *set up* and can be expensive to *live in* once the codebase is large; clean and hexagonal are expensive up front and their cost is roughly flat afterwards. Any comparison made at week one inverts the ranking you would get at year three, and neither reading is dishonest — they are answering different questions.
Locality is a property of whether the boundaries match the change history, not of the style name. A vertical slice cut along the wrong capability lines has terrible locality, and a layered codebase with only one real feature has perfect locality. The only honest way to compare these columns is to open the last thirty merged changes in your own repository and count the directories each one touched.
Every column here is a claim about *fast tests without infrastructure*, and any of the five achieves that as soon as dependencies are injected rather than constructed — which is a separate decision none of these styles owns. What differs is the default test boundary each one nudges you toward, and that matters more than the theoretical maximum: layered nudges toward class-level tests with mocks, vertical slice toward feature-level tests, and the difference shows up in how much your suite has to change during a refactor.
The columns are answering to different pressures: hexagonal responds to *external* volatility, clean to *domain* richness, vertical slice to *feature count*, and modular monolith to *team count*. A system can score high on one pressure and low on the rest, which is why picking a style from a comparison table rather than from your own pressures is how teams end up with four rings around a CRUD application.
Team fit is not a tiebreaker, it is often the deciding factor, and it is the one this table cannot capture. A structurally superior design that the team will not maintain under deadline degrades into the worst version of itself — half-applied clean architecture, with some code respecting the ring rule and some not, is harder to work in than consistent layering. The right question is which of these your team will still be following in eighteen months.
This row compares familiarity, not intrinsic difficulty, and familiarity is a property of the industry at a moment in time rather than of the design. Layered wins here largely because it is what most people have seen, which is an argument for it and also the reason it is over-applied. It is also worth separating cost-to-read from cost-to-contribute-correctly: vertical slice inverts on those two, and the table's single number hides it.
Ceremony is only waste when the feature did not need it, and every column here is right for some features and wrong for others in the same codebase. That is the actual finding of this row: a uniform ceremony level applied to every feature guarantees you are overpaying on the simple ones or underpaying on the complex ones. Allowing different features to carry different amounts of structure is more valuable than choosing which column to standardise on.
shared/ directory that grows back under a new name, or slices that each reimplement infrastructure slightly differently.Every one of these degradations is the style's own strength taken past the point where it repays — which is why none of them can be called wrong, and why §138 forbids teaching any as mandatory. What makes a codebase bad is not the column it started in but the absence of anyone asking whether the structure still matches the changes arriving. The right comparison to make is between your current structure and your last thirty changes, not between two names on a page.