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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *