DEV Community

Cover image for Composition vs Inheritance: Why Modern Software Often Prefers Composition
Ashay Tiwari
Ashay Tiwari

Posted on

Composition vs Inheritance: Why Modern Software Often Prefers Composition

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...");
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we create different birds.

class Sparrow extends Bird {}

class Eagle extends Bird {}
Enter fullscreen mode Exit fullscreen mode

Everything looks good.

Then someone asks us to add a Penguin.

Naturally, we do this:

class Penguin extends Bird {}
Enter fullscreen mode Exit fullscreen mode

But now we have a problem.

Penguins don't fly.

Yet every Penguin automatically inherits the fly() method.

const penguin = new Penguin();

penguin.fly();
Enter fullscreen mode Exit fullscreen mode

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.");
  }
}
Enter fullscreen mode Exit fullscreen mode

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

InheritanceVSComposition

Rather than making every bird inherit fly(), we separate flying into its own behavior.

class FlyingBehavior {
  fly() {
    console.log("Flying...");
  }
}
Enter fullscreen mode Exit fullscreen mode

Now a Sparrow can have flying behavior.

class Sparrow {
  flyingBehavior = new FlyingBehavior();
}
Enter fullscreen mode Exit fullscreen mode

A Penguin simply doesn't.

class Penguin {}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
bajpai_coachingclasses_3 profile image
Bajpai Coaching classes

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?

Collapse
 
ashay_tiwari_3658168ad5db profile image
Ashay Tiwari

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.