Object-oriented design comparison

Composition vs Inheritance in OOP

One of the most enduring debates in object-oriented programming. Both are mechanisms for code reuse, but they differ in coupling, flexibility, and maintainability. The principle "favour composition over inheritance" has been canonical since the Gang of Four book in 1994 — yet inheritance is still taught first everywhere.

TL;DR

  • Inheritance (is-a) — a class extends another, inheriting behaviour. Simple to start with but creates tight coupling; deep hierarchies become fragile and hard to change.
  • Composition (has-a) — a class contains other objects and delegates to them. More flexible, easier to test, and less brittle. The modern default.
  • "Favour composition over inheritance." Use inheritance for genuine, stable is-a relationships. Use composition for behaviour reuse.

Side-by-side comparison

AspectInheritanceComposition
Relationshipis-a (Dog is an Animal)has-a (Car has an Engine)
CouplingTight — subclass depends on base internalsLoose — depends only on interface/contract
FlexibilityLow — hierarchy fixed at compile timeHigh — swap components at runtime
Code reuseImplicit — inherited automaticallyExplicit — delegate calls to contained object
Deep hierarchyFragile base class problem; hard to changeNo hierarchy; each class stays small
TestabilityHarder — must instantiate full hierarchyEasier — inject mock objects
Multiple behaviourLimited by single inheritance (most languages)Combine many components freely
Best forShallow type hierarchies, framework base classesComplex behaviour built from small focused pieces

Code side-by-side

A logger with different output targets:

Inheritance (tight coupling)

class Logger {
  log(msg) { /* base logging */ }
}
class FileLogger extends Logger {
  log(msg) {
    super.log(msg);         // fragile: base dependency
    writeToFile(msg);
  }
}
class CloudLogger extends FileLogger {
  // Must extend FileLogger even if you
  // only want cloud — forced hierarchy
  log(msg) {
    super.log(msg);
    sendToCloud(msg);
  }
}

Composition (loose coupling)

class Logger {
  constructor(transports) {
    this.transports = transports; // injected
  }
  log(msg) {
    this.transports.forEach(t => t.write(msg));
  }
}

// Mix and match any transports:
const logger = new Logger([
  new ConsoleTransport(),
  new FileTransport('./app.log'),
  new CloudTransport(API_KEY),
]);
// Swap or add transports without subclassing

When to use Inheritance

  • Genuine, stable is-a relationships. ElectricCar extends Car or AdminUser extends User — the relationship is conceptually true and unlikely to change.
  • Framework base classes. extends React.Component, extends BaseModel — the framework defines the contract; you override specific hooks.
  • Shallow hierarchies (max 2 levels). One or two levels of inheritance are usually fine; three or more becomes a maintenance problem.
  • Polymorphism via a shared interface. When you need to treat all instances of a type uniformly (Animal.speak() called on a Cat or a Dog), inheritance is the natural fit.

When to use Composition

  • Behaviour from multiple sources. Most languages do not support multiple inheritance. Composition lets a class incorporate behaviours from as many sources as needed.
  • Behaviour that needs to change at runtime. Inject a different strategy, transport, or formatter at construction time or even dynamically — inheritance cannot do this.
  • Unit testing. Replace real dependencies with mocks by injecting them. Deeply inherited classes make mocking painful.
  • Any time you reach for inheritance "just for code reuse". If the only reason to inherit is to share a method, extract that method into a collaborator and compose instead.

English phrases engineers use

Inheritance conversations

  • "AdminUser extends User and overrides the permissions check."
  • "We have a fragile base class — changing it breaks three subclasses."
  • "The hierarchy is four levels deep — it's getting hard to follow."
  • "Call super() first or the parent constructor won't run."
  • "The Liskov Substitution Principle says subclasses must be replaceable for their base type."

Composition conversations

  • "Favour composition over inheritance — inject the behaviour."
  • "The class delegates to the payment processor; it doesn't know the implementation."
  • "We inject the logger so we can mock it in tests."
  • "This is the Strategy pattern — swap algorithms at runtime."
  • "Has-a is more flexible than is-a here."

Quick decision tree

  • Genuine is-a relationship, shallow hierarchy → Inheritance
  • Reusing behaviour from multiple sources → Composition
  • Need to swap behaviour at runtime → Composition
  • Framework requires extending a base class → Inheritance (no choice)
  • Deep hierarchy is forming (3+ levels) → Refactor to Composition
  • Need to unit-test with mocks → Composition (inject dependencies)
  • "I want to reuse this method" → Composition (not the right reason for inheritance)

Frequently asked questions

What is inheritance in OOP?

Inheritance (is-a relationship) lets a class extend another class, inheriting its properties and methods. A Dog class that extends Animal automatically has all of Animal's behaviour. It creates a type hierarchy. The extending class can override methods to customise behaviour.

What is composition in OOP?

Composition (has-a relationship) means a class contains instances of other classes rather than extending them. A Car class has an Engine, a Gearbox, and four Wheels — it does not extend Engine. Behaviour is delegated to the contained objects. This keeps each class small and focused.

What does "favour composition over inheritance" mean?

It is one of the most quoted principles from the Gang of Four Design Patterns book (1994). The advice is to prefer building complex behaviour by combining small, focused objects rather than building deep inheritance hierarchies. Deep hierarchies become fragile — changing a base class can break all subclasses unexpectedly.