AlgoInsight

Free lesson · JavaScript Basic to Advance

Composition vs Inheritance

Easy5 min read

The Most Important OO Decision

Inheritance and composition are the two main ways to share behavior between classes. Knowing when to choose each one is one of the most important judgment calls in object-oriented design — and one of the easiest places for engineers to get into trouble.

The short version: prefer composition. Use inheritance only for clear "is-a" relationships.

// Inheritance — "Dog IS AN Animal"
class Animal {
  walk() { return `${this.name} is walking`; }
}

class Dog extends Animal {
  constructor(name) { super(); this.name = name; }
  bark() { return `${this.name} says woof`; }
}

// Composition — "Engine is PART OF Car"
class Engine {
  start() { return "Engine started"; }
  stop()  { return "Engine stopped"; }
}

class Car {
  constructor() {
    this.engine = new Engine();   // car HAS AN engine
  }

  start() { return this.engine.start(); }
  stop()  { return this.engine.stop(); }
}

const rex = new Dog("Rex");
console.log(rex.walk());

const car = new Car();
console.log(car.start());

Dog is a kind of Animal. Inheritance fits. Car contains an Engine. Composition fits. Reversing those — saying "a Car IS AN Engine" or "Dog HAS A walk" — would model the relationships incorrectly.

This Topic covers the trade-offs in depth. After fifteen years of doing OO design at Amazon and seeing thousands of code reviews, I've come to a clear position: inheritance is overused in most codebases, and most engineers benefit from leaning harder on composition.

What Inheritance Gives You

Inheritance gives you three things:

Code reuse. The child class gets all the parent's methods automatically.

Type relationships. instanceof Animal is true for any Dog, allowing polymorphic code: "I can accept any Animal, including subclasses."

Conceptual modeling. "A Dog is an Animal" maps real-world relationships into code structure.

These are real benefits. When the relationship genuinely is "is-a," inheritance produces clean, expressive code. The problems start when people use inheritance for code reuse without the actual "is-a" relationship being there.

Interactive animation: A RobotDog that barks but does not eat has no honest seat in the Animal tree - it inherits eat() anyway.

What Composition Gives You

Composition gives you the same code reuse without the type relationship coupling:

// Inheritance approach
class Animal {
  walk() { return "walking"; }
}

class Dog extends Animal {
  // inherits walk()
}

// Composition approach
const walker = {
  walk() { return "walking"; }
};

class Dog {
  constructor() {
    Object.assign(this, walker);   // mix walker behavior into Dog
  }
}

const inheritedDog = new (class extends Animal {})();
const composedDog = new Dog();

console.log(inheritedDog.walk());
console.log(composedDog.walk());

Both Dogs can walk. The difference is in how. Inheritance ties Dog to Animal — change Animal and you affect every Animal subclass. Composition copies the walker behavior into Dog without creating a structural relationship — change walker and only future copies use the new version.

For most "I need behavior reuse" cases, composition is the safer choice because it doesn't create the type-coupling that inheritance does.

Interactive animation: Talents as parts - a dog spreads in barking and eating, while the robot dog takes only barking.

The Brittleness of Deep Inheritance

The biggest problem with inheritance is that it creates tight coupling between parent and child. When you change the parent, you have to verify that you haven't broken any child class. The more children, and the deeper the chain, the more coupling.

class Animal {
  describe() {
    return `${this.name}, age ${this.age}`;
  }
}

class Mammal extends Animal {
  describe() {
    return `${super.describe()}, mammal`;
  }
}

class Dog extends Mammal {
  describe() {
    return `${super.describe()}, dog`;
  }
}

class Labrador extends Dog {
  describe() {
    return `${super.describe()}, Labrador`;
  }
}

const lab = new Labrador();
lab.name = "Rex";
lab.age = 5;
console.log(lab.describe());

Imagine you want to add breed to the describe output. You have to think about which class to modify. Modify Labrador? Only Labradors get the new field. Modify Animal? Now Mammal and Dog also describe themselves with a breed they don't have. The right answer depends on what "describe" should mean at each level — and that decision affects every subclass.

This is the brittleness problem. Inheritance hierarchies want to grow. Each new requirement tempts you to either add another level or to modify an existing one. Both options have costs.

Composition avoids this by treating behaviors as separable pieces:

const describer = {
  describe(self) {
    return `${self.name}, age ${self.age}, ${self.type}, breed ${self.breed}`;
  }
};

class Labrador {
  constructor(name, age) {
    this.name = name;
    this.age = age;
    this.type = "Mammal/Dog";
    this.breed = "Labrador";
  }
}

const lab = new Labrador("Rex", 5);
console.log(describer.describe(lab));

The describer is a standalone behavior. The Labrador class is just data. Need a new field? Add it to the data, update the describer. Need a different describe behavior? Use a different describer. The two are decoupled.

The Is-A vs Has-A Test

The classic test for choosing between inheritance and composition:

graph TD
    A[New relationship between classes] --> B{Is-a or Has-a?}
    B -->|Is-a| C[Inheritance]
    B -->|Has-a or behavior reuse only| D[Composition]

    C --> E[Strong type coupling]
    D --> F[Loose coupling, flexible]

    style C fill:#f59e0b,color:#fff
    style D fill:#10b981,color:#fff
This diagram shows the basic decision framework. When you have a new relationship between classes, ask whether it's is-a or has-a. Is-a relationships are appropriate for inheritance, but you should be aware they create strong type coupling. Has-a relationships, or relationships where you only want behavior reuse, are appropriate for composition, which is more flexible and looser-coupled.

If you find yourself stretching the is-a relationship to justify inheritance — "well, a Manager kind of is a Reporter because they both can be assigned tasks..." — that stretching is the signal. Reach for composition instead.

Interactive animation: Has-a, not is-a - an ApiClient holds a logger and a retrier and delegates the work to them.

Mixins: Composition with Methods

A practical pattern: use mixins to compose method bundles into classes. A mixin is just a function that adds methods to a target.

const swimmable = {
  swim() {
    return `${this.name} is swimming`;
  }
};

const flyable = {
  fly() {
    return `${this.name} is flying`;
  }
};

class Duck {
  constructor(name) {
    this.name = name;
    Object.assign(this, swimmable, flyable);
  }
}

class Fish {
  constructor(name) {
    this.name = name;
    Object.assign(this, swimmable);
  }
}

class Eagle {
  constructor(name) {
    this.name = name;
    Object.assign(this, flyable);
  }
}

console.log(new Duck("Donald").swim());
console.log(new Duck("Donald").fly());
console.log(new Fish("Nemo").swim());
console.log(new Eagle("Sam").fly());

Three classes, three different combinations of behaviors. With pure inheritance, you'd need a separate SwimmingFlyingAnimal parent class for Duck — and what happens when you add a runnable mixin and need Dog and Cat to compose differently?

Mixins let each class compose only the behaviors it needs. This is more flexible than inheritance and avoids the deep-hierarchy problem entirely. It's also closer to how systems are actually structured in practice — most real "things" have multiple orthogonal capabilities, not a single inheritance lineage.

The trade-off: mixins don't give you the instanceof type relationship inheritance does. A Duck isn't instanceof Swimmable because Swimmable is an object, not a class. For most code this doesn't matter.

When Inheritance Wins

To be balanced about it: inheritance is the right choice in several specific cases.

Domain modeling with clear hierarchies. Geometric shapes (Circle, Square, Triangle all extend Shape). UI components in a framework (Button, Input, Form all extend Component). API resources (User, Post, Comment all extend ApiResource).

Sharing infrastructure across closely related types. A base class for all controllers in a web framework. A base class for all model classes in an ORM. These cases benefit from inheritance because the shared behavior is genuinely shared by all variants.

Single, well-defined abstractions. When the parent class is a single, clear concept that subclasses specialize, inheritance reads cleanly. The problems start when parents try to be many things at once.

The rule of thumb: inheritance for type hierarchies, composition for behavior reuse.

Interactive animation: A whiteboard rule for sharing behavior - inherit for a true is-a, compose to mix talents, extend a framework's base class.

Common Pitfalls

Warning: The composition vs inheritance traps.

Using inheritance for code reuse only. If you're inheriting just to access a method, but the relationship isn't really "is-a," you're creating coupling for nothing. Composition gives you the same code reuse without the type coupling.

Deep inheritance hierarchies. Three or four levels deep is usually a sign that the abstraction is wrong. Refactor toward composition before adding another layer.

Inherit everything by default. New code path → new class → extend the nearest existing one. This pattern produces sprawling hierarchies. Instead, ask whether the behavior could be a composable mixin or a separate service.

Why This Matters in Interviews

Interview: "When would you choose inheritance over composition?" is a classic OO design interview question. The expected answer: inheritance for clear is-a relationships and stable hierarchies; composition for behavior reuse, has-a relationships, and anywhere flexibility matters more than type modeling.

Senior candidates are expected to discuss the "favor composition over inheritance" principle and explain why. Citing Effective Java (Joshua Bloch) or the Gang of Four book signals OO design literacy.

Topic 8.7 — Interview Lens: this, Classes, and OO Patterns — covers the deeper interview treatment.

Try It

Exercise: Take this inheritance-heavy design and refactor it into a composition-based design:

class Vehicle { /* base */ }
class MotorVehicle extends Vehicle { /* adds motor */ }
class LandMotorVehicle extends MotorVehicle { /* adds wheels */ }
class Car extends LandMotorVehicle { /* adds 4-seat passenger area */ }
class SportsCar extends Car { /* adds boost */ }

Identify the behaviors that each level adds. Then create a composition-based version where Car composes Motor, Wheels, PassengerArea, and SportsCar additionally composes Boost. Compare the two designs:

  • Which is easier to read?
  • Which is easier to extend (add a new variant)?
  • Which would survive a requirement change like "now boats can also have boosts"?

The exercise has no single right answer — but doing it forces the trade-offs into clarity. After you've done it once, you'll spot inheritance-overuse patterns in real codebases.

Back to JavaScript Basic to Advance