skip to content

Composition Over Inheritance

Inheritance binds a subclass to the internals of its parent, so composition and delegation usually give you the same reuse with far less fragility. You will learn the fragile base-class problem and the narrow cases where inheritance is still the right call.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

6

What does the design guideline "favor composition over inheritance" actually mean, and what is the difference between the two techniques?

level: juniorimportance: must knowfreq 78%

answer

  1. is-a vs has-a
  2. inherits internals vs uses interface
  3. compile-time fixed vs runtime swappable
  4. accidental API leak (Stack extends ArrayList)
  5. forwarding boilerplate is the cost

basics

~20 s

Inheritance reuses code by making a new type a subtype of an existing one ("is-a"). Composition reuses code by holding another object as a field and calling it ("has-a"). Prefer composition: it couples types less and can change at runtime.

solid answer

~50 s

Both are code-reuse mechanisms. With inheritance, a subclass declares that it *is* a kind of its parent and automatically gets the parent's fields and methods; the relationship is fixed at compile time and the subclass sees (and depends on) the parent's internals and self-calls. With composition, a class holds a reference to a collaborator and forwards work to it; it depends only on the collaborator's public interface, the collaborator can be swapped at construction or runtime, and one class can combine several collaborators. "Favor composition" says: make reuse the default via has-a, and reach for inheritance only when you genuinely need subtype substitutability plus a stable, intentionally-designed base. Composition avoids the fragile base class problem, avoids the combinatorial explosion of subclasses when behavior varies along several axes, and keeps the subclass's public API from accidentally inheriting methods it shouldn't expose.

code

pseudocode · 11 lines
pseudocode
// Inheritance: leaks the parent's whole API and depends on its internals
class Stack extends ArrayList {
  fun push(x) = add(x)              // callers can still do stack.remove(3)
}

// Composition: only the intended API is exposed
class Stack {
  private val items = ArrayList()   // has-a
  fun push(x) { items.add(x) }
  fun pop() = items.removeLast()
}

go deeper

for a junior

Define is-a vs has-a, give one example of each, and state that composition is the safer default.

for a middle

Add the concrete failure modes — fragile base class, API leakage, single-inheritance limit, subclass explosion — and name the cost (forwarding boilerplate).

for a senior

Separate subtyping from implementation reuse, tie the legitimate uses to LSP and "designed for extension", and mention the self-problem as composition's real limitation.

for a principal

Frame it as a coupling/evolution decision: inheritance publishes your internals as a contract you must keep across versions, so it's mainly justified inside one ownership boundary or for closed hierarchies; discuss how you'd steer a codebase toward composition without a rewrite.

## The two mechanisms **Inheritance ("is-a", implementation inheritance).** A class *B* extends class *A*. *B* gets *A*'s state and behavior automatically, can override some of it, and — crucially — instances of *B* can be used anywhere an *A* is expected. Two things travel together here, and conflating them is the root of most confusion: 1. **Subtyping** — the promise "a *B* can stand in for an *A*" (the substitutability contract). 2. **Implementation reuse** — "*B* doesn't have to rewrite *A*'s code." **Composition ("has-a", delegation).** A class *B* holds a reference to an object of type *A* (usually typed by an interface) and calls it to do work. *B* exposes whatever API it wants; nothing leaks automatically. ``` // inheritance // composition class Stack extends ArrayList {} class Stack { private List items; push(x){items.add(x);} } ``` The inheritance version accidentally exposes `get(int)`, `remove(int)`, `insert` — a `Stack` that lets you poke at the middle is no longer a stack. The composition version exposes only `push`/`pop`. ## Why composition is the default - **Weaker coupling.** Composition depends on the collaborator's *published interface*. Inheritance depends on the base class's *implementation details*: which methods call which other methods, what invariants the constructor establishes, what protected state means. Those details are not usually part of a documented contract, so they change — and the subclass silently breaks. That's the **fragile base class problem**. - **Runtime flexibility.** The collaborator can be chosen by configuration, injected for tests, or swapped mid-flight (`setCompressor(gzip)`); a superclass is baked in at compile time. - **Multiple axes of variation.** If behavior varies along axes *storage × format × auth*, inheritance needs one subclass per combination (2×3×2 = 12 classes); composition needs 2+3+2 = 7 small parts assembled at runtime. - **No single-inheritance limit.** Most languages allow only one superclass but unlimited fields. Composition scales to many collaborators. - **Better encapsulation of the API surface.** A subclass inherits its parent's whole public API whether or not that makes sense (see the Stack example above). - **Testability.** A collaborator is trivially replaced with a stub/fake; overridden superclass behavior is not. ## What composition costs - **Boilerplate / forwarding code.** Wrapping a 40-method interface means writing 40 one-line forwarders (some languages help: Kotlin's `by`, Go's struct embedding, C#'s extension-plus-interface idioms, Groovy `@Delegate`). - **An extra indirection** — usually irrelevant, occasionally hot-path relevant. - **The self-problem.** In inheritance, a base method calling `this.foo()` dispatches to the override. A wrapped object calling its own `foo()` cannot see the wrapper — so behavior you expected to intercept isn't intercepted. Decorator-style designs have to be aware of this. - **More moving parts to wire up**, which is why dependency injection tends to accompany composition-heavy designs. ## When inheritance still wins Use it when there is a **true is-a relationship** that satisfies the **Liskov Substitution Principle** (every place that works with the base must keep working with the subtype), and when the base was **designed for extension** — documented self-calls, well-chosen extension points, ideally in the same codebase/team so the base and its subclasses evolve together. Interface inheritance (implementing a pure interface / abstract type with no inherited implementation) carries none of the fragility and is not what the guideline warns about; the guideline targets **implementation inheritance**. Sealed/closed hierarchies used for exhaustive matching (an expression AST: `Add | Mul | Literal`) are another legitimate, safe use — the whole hierarchy is known and owned in one place. ## The one-line rule of thumb Ask "is *B* a kind of *A* everywhere, forever?" If you're only after the code, use composition. If you also want the substitutability and you own the base, inheritance is fine.

  • Does "favor composition over inheritance" also apply to implementing interfaces?
    No. The guideline targets *implementation* inheritance, where you inherit code and internal assumptions. Implementing a pure interface/abstract type adds a subtyping promise but no inherited implementation, so none of the fragility applies — that's normally encouraged.
  • Give a concrete symptom that a hierarchy should have been composition.
    Overrides that throw `UnsupportedOperationException` or return "not applicable", subclasses that must be careful about the *order* they call `super`, or a subclass count that multiplies whenever a new independent option appears.

Inheritance is being born into a family — you get the whole genome, quirks included, and you can't change parents. Composition is hiring a contractor: you specify the job, you can fire and rehire, and you never inherit their personal habits.

context

open as a page

What is the fragile base class problem, and can you walk through a concrete way a harmless-looking change in a base class breaks its subclasses?

level: middleimportance: must knowfreq 62%

basics

~20 s

When a subclass overrides methods, it can depend on how the base class calls its own methods internally. If the base later changes those internal calls — even without changing its public behavior — subclasses silently break. That's the fragile base class problem.

open as a page

When is class inheritance still the right choice over composition? Give concrete criteria, not just "when there's an is-a relationship".

level: seniorimportance: must knowfreq 55%

basics

~20 s

Use inheritance when the subtype is genuinely substitutable for the base everywhere (Liskov), the base was deliberately designed and documented for extension, and both live under one owner. Closed/sealed hierarchies and framework template-method hooks are good fits.

open as a page

A hierarchy has grown to dozens of subclasses because behavior varies along several independent axes (e.g. storage backend × serialization format × compression). How would you restructure it, and what is this failure mode called?

level: middleimportance: should knowfreq 48%

basics

~20 s

It's a combinatorial subclass explosion: with inheritance you need one class per combination (2×3×2 = 12). Replace each axis with a composed collaborator chosen by interface — a Strategy per axis — so you write 2+3+2 = 7 parts and assemble them.

open as a page

Composition usually means delegation. What is delegation in practice, what boilerplate and pitfalls does it bring, and what is the "self-problem"?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Delegation means an object forwards calls to a collaborator it holds. You get flexibility, but must write a forwarder per method, and the wrapped object's internal calls dispatch to itself — so your wrapper can't intercept them. That's the self-problem.

open as a page

You own a widely-used library. What is your policy on letting consumers subclass your classes, and how does that policy affect versioning and long-term evolution?

level: principalimportance: nice to knowfreq 22%

basics

~10 s

Default to closed classes (final/sealed) and offer extension through documented interfaces, callbacks, or injected strategies. Subclassing exposes your internal call structure as a contract you can never change without breaking consumers.

open as a page