Summary
Composition Over Inheritance is a design principle that favors assembling behavior from small, independent components over building deep inheritance hierarchies. Inherited behavior is implicit and hard to change; composed behavior is explicit, targeted, and independently testable.
Problem
Deep inheritance chains couple subclasses to the internal structure of parent classes. Changes to base classes ripple through the entire hierarchy. Behavior cannot be mixed and matched across subtypes without violating the hierarchy.
- How do you reuse
Serializablebehavior in a class that already extendsBaseEntity? - Why does fixing a bug in
Animal.move()breakFlyingAnimal,SwimmingAnimal, andRunningAnimal? - How do you share behavior between two classes that have no meaningful “is-a” relationship?
Solution
Delegate behavior to injected or composed collaborators rather than inheriting it.
// Inheritance — rigid, coupled
class FlyingFishAnimal extends SwimmingAnimal {
// forced to pick one base; flying behavior must be duplicated or hacked in
}
// Composition — flexible, explicit
interface Swimmer { swim(): void; }
interface Flier { fly(): void; }
class Fish implements Swimmer {
swim() { console.log("swimming"); }
}
class FlyingFish {
constructor(
private swimmer: Swimmer,
private flier: Flier
) {}
swim() { this.swimmer.swim(); }
fly() { this.flier.fly(); }
}
Each capability is a focused, independently testable unit. FlyingFish can combine them without inheriting from either.
The same shape holds in Python, using composition instead of a multiple-inheritance chain:
# Inheritance — rigid, coupled
class FlyingFishAnimal(SwimmingAnimal):
# forced to pick one base; flying behavior must be duplicated or hacked in
pass
# Composition — flexible, explicit
class Swimmer:
def swim(self) -> None:
print("swimming")
class Flier:
def fly(self) -> None:
print("flying")
class FlyingFish:
def __init__(self, swimmer: Swimmer, flier: Flier):
self._swimmer = swimmer
self._flier = flier
def swim(self) -> None:
self._swimmer.swim()
def fly(self) -> None:
self._flier.fly()
# FlyingFish combines both capabilities without extending either class
flying_fish = FlyingFish(Swimmer(), Flier())
Python’s multiple inheritance can approximate composition via mixins, but it reintroduces the fragile base class problem the moment two mixins share a method name. Explicit composition avoids the ambiguity entirely — FlyingFish owns its collaborators rather than merging their namespaces.
Live Playground
Experiment with the pattern below. In the before version, FlyingFish can extend only one base class, so it extends SwimmingAnimal and carries a copy of fly(). When a bug fix caps flight altitude in FlyingAnimal, the copy doesn’t get it. Penguin extends FlyingAnimal because a penguin is a bird, so it has to override fly() to throw, and a loop over every FlyingAnimal crashes on it. In the after version, swimming and flying are small collaborators that each animal composes as it needs. The altitude fix lives in one Flying class and reaches every animal that uses it. Penguin never gets a fly() method, so the compiler keeps it out of the list of fliers.
When to Use
- When behavior needs to be reused across classes that don’t share a meaningful “is-a” relationship
- When a class hierarchy is growing beyond 2–3 levels
- When you need runtime flexibility to swap behaviors (often combined with Strategy Pattern)
- When inherited behavior is being overridden to do nothing — a sign the hierarchy is wrong
Avoid when:
- A genuine “is-a” relationship exists and the parent class is stable (e.g., value types extending a base value object)
- Composition creates excessive boilerplate for simple, stable hierarchies
Trade-offs
| Benefit | Cost |
|---|---|
| Behavior is explicit — you can see exactly what a class does | More wiring and delegation boilerplate |
| Capabilities are independently testable and reusable | Can require more files and interfaces |
| No fragile base class problem — changes are localized | Requires discipline to avoid deep composition chains |
| Runtime behavior can be varied without subclassing | Discoverability can be harder than method inheritance |
Related Patterns
- Strategy Pattern — The behavioral pattern most directly enabled by composition
- Dependency Injection — The mechanism for injecting composed behaviors
- Single Responsibility — Composed units should each have a single responsibility
- Component Composition — The same principle applied in the UI layer: small, focused components assembled rather than deep component hierarchies
- Micro-Frontend Architecture — Composition at the application boundary: independently deployed remotes assembled into a shell