Composition over Inheritance
Purpose
Pick the cheapest relationship that does the job. Implementation inheritance is one of the
strongest source-level couplings Java offers: a subclass depends not only on the base contract
but on its self-use — which methods call which, in what order, touching what state — none
of which the compiler checks and most of which is undocumented. This skill exists to
prevent two failures: reuse-by-extends that turns every base-class edit into a minefield,
and dogmatic decomposition that replaces a sound three-class hierarchy with a swarm of
delegating wrappers.
Workflow
- Inspect the compatibility and ownership boundary. Check compiler release/toolchains,
CI runtime, external subclasses and separately deployed clients before changing
extends
or permits. The worked example uses Java 21 without preview; sealed types require
Java 17+, and pattern switches require Java 21+ without preview. On an older target, use
ordinary composition/polymorphism rather than upgrading or enabling preview implicitly.
- Name what is being inherited. A contract (the subtype is usable wherever the base
is), implementation (code reuse only), or both. Reuse without substitutability is the
case to eliminate: hold the other object in a field and forward.
- If the variants form a closed set you own, consider a sealed interface. Put stable
variant-owned behavior on the implementations; use an exhaustive
switch when operations
evolve more often than variants. Recompilation finds missing cases, while already compiled
clients can instead encounter MatchException after incompatible hierarchy evolution.
- If behaviour varies along more than one independent axis, calculate the subtype product.
When N×M classes or override-order knowledge appears, keep at most one axis as a hierarchy
and compose the others as policies. Correlated axes with a tiny closed product may still be
clearer as named subtypes.
- If genuine substitutability remains, inheritance is right — see the rule below for
common legitimate shapes. Design and document for it: specify self-use, keep
overridable surface minimal.
- Decide with evidence. Read
references/decision-model.md for the fragile-base risk
heuristics, the decision table and the false positives. For executing a migration off a
hierarchy, read references/worked-refactoring.md.
Rules
- An externally subclassable class not designed for extension should be
final, sealed, or
hidden behind a non-exported/package-private boundary. Framework proxies and bytecode tools
can require non-final classes; treat that as an explicit runtime contract with tests.
- Never call an overridable method from a constructor: it runs against a subclass whose
fields are not yet initialised. Self-use of overridable methods elsewhere must be
documented, because subclasses will depend on it either way.
- A subclass that overrides a promised operation to do nothing or throw is evidence the base
contract is too broad. Restructure code you own; for a platform contract that explicitly
permits optional operations, document and test the chosen partial behavior instead of
pretending the exception cannot occur.
- Records are implicitly final and cannot declare a superclass: they extend
java.lang.Record.
They can implement open or sealed interfaces; seal only when the family should be closed.
- Inheritance is justified when substitutability holds under a stable documented contract and
shared implementation/state is worth its evolution coupling. Common sound shapes include a
framework template explicitly designed for extension and a shallow sealed abstract base for a
closed same-module family; exception classification and compatibility adapters can also be
contract hierarchies without sharing algorithms.
- Composition has costs — forwarding boilerplate, distinct identity (
wrapper != wrapped),
possible equality/listener mismatches and fluent returns that may expose the delegate.
Inspect and test those contracts before dismantling a working hierarchy; do not present delegation
as free.
Deliverable
Name the observed coupling or contract defect, ownership/compatibility constraints, chosen
relationship and the trade-off that rules out the closest alternative. For a migration,
map old entry points to new ones and distinguish preserved behavior from policy changes;
report characterization checks actually run. If callers or subclass contracts are unavailable,
state the missing evidence and keep claims of safe replacement conditional.
References
- Decision model — inheritance vs composition vs sealed
hierarchy decision table, fragile-base risk heuristics, and false positives that look
like abuse but are sound. Read before recommending a restructure.
- Worked refactoring — a payment-fee inheritance
hierarchy replaced by a sealed type plus composed policy, with what got worse. Read
when executing such a migration.
1---2name: java-composition-over-inheritance3description: Choosing between inheritance, composition and sealed hierarchies in Java: fragile base classes, self-use of overridable methods, subclass explosion, the costs of delegation and decoration, sealed types with exhaustive switch as the modern middle ground, and the cases where inheritance is genuinely right. Use when reviewing an `extends` between classes you maintain, when a base-class change broke subclasses, when variants multiply along more than one axis, or when designing a new hierarchy. Does not cover behavioural substitutability formalism (java-design-by-contract) or the SOLID framing of LSP (java-solid).4---56# Composition over Inheritance78## Purpose910Pick the cheapest relationship that does the job. Implementation inheritance is one of the11strongest source-level couplings Java offers: a subclass depends not only on the base contract12but on its self-use — which methods call which, in what order, touching what state — none13of which the compiler checks and most of which is undocumented. This skill exists to14prevent two failures: reuse-by-extends that turns every base-class edit into a minefield,15and dogmatic decomposition that replaces a sound three-class hierarchy with a swarm of16delegating wrappers.1718## Workflow19200. **Inspect the compatibility and ownership boundary.** Check compiler release/toolchains,21 CI runtime, external subclasses and separately deployed clients before changing `extends`22 or `permits`. The worked example uses Java 21 without preview; sealed types require23 Java 17+, and pattern switches require Java 21+ without preview. On an older target, use24 ordinary composition/polymorphism rather than upgrading or enabling preview implicitly.251. **Name what is being inherited.** A contract (the subtype _is_ usable wherever the base26 is), implementation (code reuse only), or both. Reuse without substitutability is the27 case to eliminate: hold the other object in a field and forward.282. **If the variants form a closed set you own**, consider a sealed interface. Put stable29 variant-owned behavior on the implementations; use an exhaustive `switch` when operations30 evolve more often than variants. Recompilation finds missing cases, while already compiled31 clients can instead encounter `MatchException` after incompatible hierarchy evolution.323. **If behaviour varies along more than one independent axis**, calculate the subtype product.33 When N×M classes or override-order knowledge appears, keep at most one axis as a hierarchy34 and compose the others as policies. Correlated axes with a tiny closed product may still be35 clearer as named subtypes.364. **If genuine substitutability remains**, inheritance is right — see the rule below for37 common legitimate shapes. Design and document for it: specify self-use, keep38 overridable surface minimal.395. **Decide with evidence.** Read40 [references/decision-model.md](references/decision-model.md) for the fragile-base risk41 heuristics, the decision table and the false positives. For executing a migration off a42 hierarchy, read [references/worked-refactoring.md](references/worked-refactoring.md).4344## Rules4546- An externally subclassable class not designed for extension should be `final`, `sealed`, or47 hidden behind a non-exported/package-private boundary. Framework proxies and bytecode tools48 can require non-final classes; treat that as an explicit runtime contract with tests.49- Never call an overridable method from a constructor: it runs against a subclass whose50 fields are not yet initialised. Self-use of overridable methods elsewhere must be51 documented, because subclasses will depend on it either way.52- A subclass that overrides a promised operation to do nothing or throw is evidence the base53 contract is too broad. Restructure code you own; for a platform contract that explicitly54 permits optional operations, document and test the chosen partial behavior instead of55 pretending the exception cannot occur.56- Records are implicitly final and cannot declare a superclass: they extend `java.lang.Record`.57 They can implement open or sealed interfaces; seal only when the family should be closed.58- Inheritance is justified when substitutability holds under a stable documented contract and59 shared implementation/state is worth its evolution coupling. Common sound shapes include a60 framework template explicitly designed for extension and a shallow sealed abstract base for a61 closed same-module family; exception classification and compatibility adapters can also be62 contract hierarchies without sharing algorithms.63- Composition has costs — forwarding boilerplate, distinct identity (`wrapper != wrapped`),64 possible equality/listener mismatches and fluent returns that may expose the delegate.65 Inspect and test those contracts before dismantling a working hierarchy; do not present delegation66 as free.6768## Deliverable6970Name the observed coupling or contract defect, ownership/compatibility constraints, chosen71relationship and the trade-off that rules out the closest alternative. For a migration,72map old entry points to new ones and distinguish preserved behavior from policy changes;73report characterization checks actually run. If callers or subclass contracts are unavailable,74state the missing evidence and keep claims of safe replacement conditional.7576## References7778- [Decision model](references/decision-model.md) — inheritance vs composition vs sealed79 hierarchy decision table, fragile-base risk heuristics, and false positives that look80 like abuse but are sound. Read before recommending a restructure.81- [Worked refactoring](references/worked-refactoring.md) — a payment-fee inheritance82 hierarchy replaced by a sealed type plus composed policy, with what got worse. Read83 when executing such a migration.