In the previous article, we learned that Inheritance was introduced to solve a very real problem—code duplication.
Instead of writing the same behavior repeatedly, we could move common functionality into a parent class and let child classes inherit it.
It sounded like the perfect solution.
For a long time, many developers thought:
"If two classes share some behavior, just use inheritance."
And that's exactly what happened.
Large applications started building deep inheritance hierarchies.
Unfortunately, a new set of problems emerged.
Let's understand why.
🤯 The Problem
Imagine we're building an application for different kinds of birds.
We start with a base class.
class Bird {
fly() {
console.log("Flying...");
}
}
Now we create different birds.
class Sparrow extends Bird {}
class Eagle extends Bird {}
Everything looks good.
Then someone asks us to add a Penguin.
Naturally, we do this:
class Penguin extends Bird {}
But now we have a problem.
Penguins don't fly.
Yet every Penguin automatically inherits the fly() method.
const penguin = new Penguin();
penguin.fly();
Technically, the code works.
Logically, it doesn't.
Our inheritance hierarchy is forcing behavior onto objects that shouldn't have it.
Trying to Fix It
One common approach is overriding the method.
class Penguin extends Bird {
fly() {
throw new Error("Penguins can't fly.");
}
}
This "fixes" the issue.
But think about it.
We inherited behavior only to disable it.
That's usually a sign that our model isn't representing reality very well.
The Root Cause
Inheritance creates a very strong relationship.
When a class extends another class, it says:
"I am a specialized version of this parent."
That's a big commitment.
The child automatically receives:
- State
- Methods
- Design decisions
- Future changes
As the parent evolves, every child is affected.
Sometimes that's exactly what we want.
Sometimes it becomes a maintenance headache.
😐 A Different Way of Thinking
Instead of asking:
"What should this object inherit?"
We can ask:
"What capabilities does this object need?"
That's the idea behind Composition.
Instead of inheriting behavior, we build objects by combining smaller, focused pieces.
✨ Composition in Action
Rather than making every bird inherit fly(), we separate flying into its own behavior.
class FlyingBehavior {
fly() {
console.log("Flying...");
}
}
Now a Sparrow can have flying behavior.
class Sparrow {
flyingBehavior = new FlyingBehavior();
}
A Penguin simply doesn't.
class Penguin {}
No unnecessary methods.
No overridden behavior.
No pretending that penguins can fly.
Each object gets only the capabilities it actually needs.
Thinking Beyond Birds
This idea appears everywhere.
Imagine a payment system.
Instead of:
Payment
├── CreditCardPayment
├── UpiPayment
├── PayPalPayment
└── CashPayment
We might compose behavior like:
- Refundable
- SupportsInstallments
- GeneratesInvoice
- RequiresAuthentication
Each payment method gets only the behaviors it needs.
This makes the system far more flexible.
Why Composition Is Often Preferred
Composition offers several advantages.
Smaller Responsibilities
Each component does one thing well.
Better Reusability
The same behavior can be shared across unrelated objects.
Lower Coupling
Changing one component doesn't necessarily affect everything else.
Greater Flexibility
Objects can gain or lose behavior simply by changing the components they use.
Does This Mean Inheritance Is Bad?
Not at all.
Inheritance is still a valuable tool.
It works well when there is a genuine "is-a" relationship.
Examples include:
- A Circle is a Shape.
- A Square is a Shape.
- A SavingsAccount is a BankAccount.
The problem isn't inheritance itself.
The problem is using inheritance simply to reuse code.
If the relationship doesn't naturally fit, inheritance often introduces more problems than it solves.
A Practical Guideline
A common piece of advice you'll hear is:
Favor Composition Over Inheritance.
Notice the wording.
It doesn't say:
Never use inheritance.
It says:
Prefer composition when it gives you a simpler and more flexible design.
Inheritance and Composition are both tools.
🔑 The Key Takeaway
Inheritance helped developers eliminate duplicated behavior.
Composition helped developers build systems that were easier to change.
Modern software often prefers composition because requirements change far more often than class hierarchies.
⏭️ What's Next?
We've now explored three important ideas in Object-Oriented Programming:
- Encapsulation protects an object's state.
- Abstraction hides unnecessary complexity.
- Composition helps us build flexible systems.
One important pillar still remains.
In the next article, we'll explore Polymorphism and understand how different objects can share a common interface while providing their own implementations.

Top comments (2)
Great article! In your experience, what’s the biggest sign that a design is becoming over-composed and inheritance might actually be the simpler choice?
Thanks @bajpai_coachingclasses_3
That's a great question. For me, it's usually a sign when I have to wire together a bunch of tiny classes just to represent a straightforward concept. If understanding a feature means jumping through five or six collaborating objects, composition may be adding more complexity than value.
In cases where there's a stable 'is-a' relationship with shared behavior that's unlikely to change, a simple inheritance hierarchy can actually be cleaner and easier to maintain.
I don't think it's about choosing one over the other—it's about picking the one that keeps the design easiest to understand.