Composition over Inheritance
Build complex behaviour by combining small, focused objects rather than extending a class hierarchy — keeping coupling low and flexibility high.
★★★★★5/5Inside a codebase — classes, modules, files
How it works
The GoF book (1994) coined the phrase 'favor object composition over class inheritance' after observing that deep inheritance hierarchies are one of the most common sources of fragile code.
Inheritance (IS-A) creates a compile-time dependency between a subclass and its parent. Any change to the base class — a new field, a changed method signature, altered behaviour — ripples to every subclass. The Fragile Base Class problem is real: a 'harmless' parent change can silently break subtypes.
Composition (HAS-A) avoids this by assembling behaviour from injected collaborators. A Duck doesn't extend Animal and FlyingThing — it holds a MoveBehavior and a FlyBehavior. Behaviours are interfaces; concrete classes are injected. Swapping the fly behaviour at runtime doesn't require a new subclass.
Inheritance is still appropriate for true IS-A relationships with stable base classes. The advice is to reach for composition *first*, and only use inheritance when the hierarchy is genuinely stable and the IS-A relationship is real.
Implementation
TypeScript · Go · Rust// ❌ Inheritance — fragile hierarchy breaks when new combinations arrive
class Animal { move() { return "moves"; } }
class Dog extends Animal { bark() { return "woof"; } }
// What about a dog that can swim AND herd? Hierarchy explodes.
// ✓ Composition — assemble behaviours as interfaces
interface Movable { move(): string; }
interface Swimmable { swim(): string; }
interface Herder { herd(): string; }
const walkBehaviour: Movable = { move: () => "walks on four legs" };
const swimBehaviour: Swimmable = { swim: () => "paddles through water" };
const herdBehaviour: Herder = { herd: () => "circles the flock" };
class BorderCollie {
constructor(
private movement: Movable,
private swimming: Swimmable,
private herding: Herder,
) {}
move() { return this.movement.move(); }
swim() { return this.swimming.swim(); }
herd() { return this.herding.herd(); }
}
const dog = new BorderCollie(walkBehaviour, swimBehaviour, herdBehaviour);
// Behaviours are independently replaceable and testableWhy it matters
Inheritance couples a subclass to its parent's internals. As hierarchies deepen, they become rigid — changing anything near the root breaks the tree. Composition keeps each piece independent and replaceable, making systems easier to test and evolve.
✓ When to use
- →When behaviour needs to vary at runtime (inject different strategies)
- →When base classes are unstable or owned by a third party
- →Mixing several unrelated capabilities into one class
- →Prefer composition by default — use inheritance only when you have a genuine IS-A relationship
✗ When NOT to use
- →Framework classes that explicitly define an extension point (React.Component, JUnit test cases)
- →True IS-A hierarchies with stable, well-understood base classes
Trade-offs
Behaviours are independently replaceable and testable
More wiring code — you must assemble the collaborators explicitly
No fragile base class problem — changes stay local
Too many tiny collaborators can scatter logic across many files
Enables runtime behaviour changes without new subclasses
Interface explosion if every behaviour gets its own interface prematurely
In production
Hooks replaced class inheritance for shared logic — compose useState, useEffect, custom hooks instead of extending Component
Go has no inheritance; struct embedding plus interface composition is the idiomatic way to reuse and extend behaviour
Traits composed on structs — no class hierarchy; multiple traits mix freely without diamond-inheritance problems
Industry adoption
Related principles
Interface / Contract
Define what a component must do without dictating how it does it — so implementations can vary freely while callers remain stable.
Strategy Pattern
Define a family of interchangeable algorithms behind a common interface so the calling context can swap them at runtime without changing its own code.
Dependency Injection
Supply a component's dependencies from the outside rather than letting it construct them — so implementations can be swapped without changing the component.
Liskov Substitution Principle
A subtype must be fully substitutable for its parent — code that works with the base type must work correctly with any derived type, without surprises.