Composition over Inheritance#
classDiagram
direction LR
class Robot {
-mover: Mover
-sensor: Sensor
-gripper: Gripper
+act()
}
class Mover
class Sensor
class Gripper
Robot o--> Mover
Robot o--> Sensor
Robot o--> Gripper
Prefer giving a class collaborators (composition) over making it a subtype of something. Inheritance commits you to a hierarchy; composition gives you a swappable bag of behaviours.
The inheritance trap#
classDiagram
class Animal {
+eat()
+sleep()
}
class Bird {
+fly()
+eat()
+sleep()
}
class Fish {
+swim()
+eat()
+sleep()
}
class Penguin {
+eat()
+sleep()
+swim()
+fly() -- throws UnsupportedOperationException
}
Animal <|-- Bird
Animal <|-- Fish
Bird <|-- Penguin
A Penguin is a Bird but can't fly. Either:
- override fly() to throw (Liskov-violating); or
- restructure the hierarchy (and rebuild your codebase).
The composition fix#
classDiagram
direction LR
class Animal {
-mover: MoveBehavior
+move()
}
class MoveBehavior {
<<interface>>
+move()
}
class Fly
class Swim
class Walk
MoveBehavior <|.. Fly
MoveBehavior <|.. Swim
MoveBehavior <|.. Walk
Animal o--> MoveBehavior
Each animal is composed of what it can do. A penguin gets Swim + Walk, no Fly. New behaviour = new class, no hierarchy edits.
Rules of thumb#
- "is-a" → inheritance (rare).
- "has-a" / "can do" → composition (default).
- If you're inheriting only to share code, prefer composition + delegation.
- If you're inheriting to be the parent type in the type system, inheritance is fine - but watch LSP.
Mixin / trait alternatives#
Languages like Scala (traits), Rust (impl blocks), TypeScript (intersection types), Python (mixins) let you mix in capabilities without single-line inheritance.
Where this shows up in this site#
- Chess engine -
Piecedoesn't subclassMoveStrategy; it has a move strategy. - Notification system -
Notificationdoesn't extendEmailDeliverer; it composes aChannel. - Logger framework -
Loggercomposes appenders + formatters instead of inheriting from them. - Vending machine / Hotel management - pricing, dispatch, and state machines composed in.
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Observability | metrics, logs, traces, SLOs | observability |
LLD |
State machines | FSM, HSM, transitions, guards | state-machines |
LLD |
Async models | futures / async-await / coroutines / actors | async-models |
LLD |
OOP pillars | encapsulation, abstraction, inheritance, polymorphism | oop-pillars |
LLD |
SOLID principles | SRP / OCP / LSP / ISP / DIP | solid-principles |
LLD |
Behavioural patterns | Strategy, Observer, State, Command, Chain | behavioral-patterns |
LLD |
Composition over inheritance | prefer composition; small swappable parts | composition-over-inheritance |
LLD |
Error handling | exceptions vs Result, error boundaries | error-handling |
Quick reference#
Why it usually wins#
- Behaviours can be swapped at runtime.
- Easier to test (mock the collaborator).
- Avoids deep hierarchies (which are hard to reason about).
- Sidesteps multiple-inheritance issues.
When inheritance is fine#
- Closed, stable hierarchies (Shape → Circle/Rect/Triangle).
- Templates and frameworks that explicitly require it (Spring's HandlerAdapter, Android Activity).
- Trivial extension where overriding is the natural mechanism.
"Favor composition" ≠ "never inherit"#
- Single-level inheritance is often perfectly fine.
- Inheritance chains > 2 levels are a smell.
- "Marker interfaces" or sealed hierarchies model algebraic data types cleanly.
Refs#
- Effective Java - Items 18 (favor composition over inheritance), 19 (design for inheritance or prohibit it).
- Design Patterns (GoF) - many patterns are composition-based alternatives to inheritance.
- Go: design and evolution talks - Go intentionally drops inheritance.
FAQ#
Why prefer composition over inheritance?#
Composition keeps behavior swappable at runtime and avoids the fragile base class problem. Inheritance hard-codes relationships at compile time and tends to leak parent details into subclasses.
When is inheritance still the right choice?#
When you have a true is-a relationship with a stable hierarchy and want to share invariants, like AbstractList in Java collections. Even then, prefer interfaces plus composition for behavior.
What is the fragile base class problem?#
Changes to a base class can silently break subclasses that depended on subtle behavior. Composition avoids this because collaborators only see public interfaces, not protected internals.
How does the Strategy pattern apply composition?#
Instead of subclassing to vary an algorithm, the class holds a Strategy collaborator that can be swapped at runtime. New behaviors become new strategy classes, not new subclasses.
Does composition cost more boilerplate?#
Yes, you write more constructors and interfaces. Modern languages with records, lambdas, or dependency injection greatly reduce this overhead and make composition the default choice.
Related Topics#
- SOLID Principles: OCP and DIP directly motivate composition over inheritance
- Structural Patterns: Decorator and Bridge are the key structural patterns that replace inheritance hierarchies
- Behavioral Patterns: Strategy and Command patterns illustrate composition in action
Further reading#
Curated, high-credibility sources for going deeper on this topic.