Composition over inheritance is a design rule of thumb: when an object needs a certain behavior, prefer giving it a reference to a small object that provides that behavior over inheriting it from a parent class. It does not say inheritance is wrong — when you are unsure which tool fits, composition tends to age better, because swapping a small collaborator is far easier than restructuring a class hierarchy later.
What changed in 2026
- Framework defaults keep nudging toward composition. More UI and application frameworks favor composing small, focused pieces (components, hooks, mixins) over deep class inheritance, making the composition-first habit feel closer to a default than a contrarian opinion.
- Interfaces and protocols got easier to lean on. Structural typing features across mainstream languages make it simpler to describe "any object with this shape" without a shared base class, which removes one of the classic reasons people reached for inheritance.
- Code review culture increasingly flags deep hierarchies on sight. More teams treat a third-level subclass as worth a direct question in review — does this need to inherit, or could it hold a reference instead — rather than waving it through.
The fragile base class problem
The core argument for composition is not abstract — it has a name: the fragile base class problem. A base class accumulates behavior over time, and every subclass depends on exactly how that behavior works. Change the base class to fix or improve it, and subclasses several levels down can break in ways the person making the change had no way to anticipate.
Composition avoids this because a collaborator object has a narrow, explicit contract. Changing it only affects objects that explicitly hold a reference to it, and that dependency is visible right in the code, not inherited invisibly from three files away.
The classic example: behavior that varies independently
// Inheritance forces every subclass into the same shape
class Bird {
fly() { return "flying"; }
}
class Penguin extends Bird {
fly() { throw new Error("penguins cannot fly"); } // awkward override
}
// Composition: behavior is a separate, swappable part
const canFly = { fly: () => "flying" };
const cannotFly = { fly: () => "cannot fly" };
function makeBird(flyBehavior) {
return { ...flyBehavior };
}
const sparrow = makeBird(canFly);
const penguin = makeBird(cannotFly);
Nothing here is forced to override a method it cannot honestly implement. Each bird just holds the behavior that applies to it, injected at creation time. This is structurally the same trick a higher-order function uses when it accepts behavior as an argument instead of hardcoding it.
When each one actually fits
| Situation |
Inheritance |
Composition |
| Relationship is genuinely stable and is-a |
Fits well |
Also works, but often unnecessary overhead |
| Behavior needs to vary per instance or change at runtime |
Forces overrides or awkward branching |
Fits well — swap the collaborator |
| Hierarchy would go more than one or two levels deep |
Gets fragile fast |
Not applicable — avoids deep chains by design |
| Sharing one behavior across otherwise unrelated types |
Awkward, tempts multiple-inheritance workarounds |
Natural — both types hold the same component |
How to refactor from inheritance to composition
- Find the method that every subclass overrides differently — that is the behavior actually varying.
- Extract that method into its own small object or function.
- Have the original class hold a reference to that behavior instead of inheriting it.
- Inject the concrete behavior at construction time through a constructor argument or factory function.
- Repeat for any other behavior that varies independently.
This is the same move behind the classic strategy pattern — composition over inheritance applied specifically to interchangeable algorithms or behaviors.
Common mistakes
Treating this as a ban on inheritance entirely. A shallow, stable is-a relationship — a Circle that is unambiguously a Shape — is not what this warns against.
Composing so many tiny objects that the wiring becomes its own maze. Composition trades hierarchy complexity for construction complexity; past a point, that trade needs its own organizing structure, like a factory or dependency container.
Refactoring stable, working inheritance purely on principle. If a hierarchy has not caused a real problem, rewriting it to follow a rule of thumb is effort spent on the wrong thing.
FAQ
Does composition over inheritance mean never use inheritance?
No. It is a default preference for uncertain cases, not a ban. Shallow, stable is-a relationships still fit inheritance well.
Is this the same as the strategy pattern?
The strategy pattern is one concrete implementation of the general principle, for making an algorithm or behavior swappable at runtime.
Does this idea apply outside object-oriented code?
Yes, conceptually. Function composition in functional programming is the same underlying idea — building behavior by combining small pieces rather than one large, rigid structure.
Is composition always more code than inheritance?
Often slightly more upfront — an interface or small object plus the wiring to inject it. That extra code tends to pay for itself the first time behavior needs to change without touching every subclass.
Where to go next