Stop Treating Inheritance and Composition Like Opposing Religions: Use the One the Code Deserves
Inheritance and composition are two fundamental ways to build relationships between parts of a software system. Both solve similar problems — code reuse, shared behavior, and structure — but they do it in very different ways.
Understanding when to choose one over the other is not an academic exercise. It affects how easily your system scales, changes, and survives real-life refactoring.
What Is Inheritance?
Inheritance creates an “is-a” relationship between classes.
class Animal {
speak() { console.log("Some sound") }
}
class Dog extends Animal {
speak() { console.log("Bark") }
}
Here Dog inherits behavior from Animal.
A dog is an animal — the relationship matches reality.
Pros
- Good when objects share identity and behavior
- Reduces duplication when the hierarchy is stable
- Enables polymorphism naturally
Cons
- Tight coupling — child depends on parent’s decisions
- Changes in base classes ripple everywhere
- Often abused to reuse code instead of model concepts
What Is Composition?
Composition creates a “has-a” or “uses” relationship by assembling behavior from other objects.
class Engine {
start() { console.log("Engine started") }
}
class Car {
constructor() {
this.engine = new Engine()
}
drive() {
this.engine.start()
}
}
A car has an engine, not is an engine.
Pros
- Loosely coupled — parts can change independently
- Encourages small, testable components
- Scales better in evolving systems
Cons
- Requires more design discipline initially
- May look more “scattered” to beginners
- More wiring needed in very simple scripts
When to Prefer Inheritance
Use inheritance when all of these are true:
- The relationship is genuinely is-a
(e.g.,User→AdminUser) - The base class models a stable abstraction
- You intend to rely on polymorphism explicitly
class Notification { send() {} }
class EmailNotification extends Notification { send() { ... } }
class SMSNotification extends Notification { send() { ... } }
The contract (send) is the point.
When to Prefer Composition
Use composition when:
- You want reuse without tight coupling
- Behavior is shared, not identity
- The system is evolving and may change shape over time
Example:
class Logger { log(msg) { ... } }
class Service {
constructor(logger) {
this.logger = logger
}
run() {
this.logger.log("Running...")
}
}
Here composition gives flexibility — tomorrow logger can be DB, file, or remote.
The Most Common Mistake Engineers Make
They use inheritance because the code “looks similar” — not because it represents a real “is-a” relationship.
That mistake almost always leads to brittle hierarchies that are painful to refactor.
The Guiding Rule
Use inheritance to express identity. Use composition to express capability.
If it is the thing → inheritance.
If it has the thing → composition.
Before You Leave — One Reality Check
Inheritance is not evil. Composition is not always superior. But in real production systems that evolve under deadlines, composition tends to survive change better.
If you take one sentence from this article:
Composition protects you from future change; inheritance commits you to it.
Your Turn
Have you seen inheritance abused — or composition taken too far?
Real war stories teach better than theory. Share yours in the comments.
And if you want more articles like this on architecture, design decisions, and real-world engineering — follow along.