Vasu Patel logoVasu Patel
← Objects First: OOP Fundamentals
All chapters in this track
  1. 01Chapter 1: Why Object Orientation Exists
  2. 02Chapter 2: Classes, Objects, and Instances, Precisely Defined
  3. 03Chapter 3: The Four Pillars I: Encapsulation
  4. 04Chapter 4: The Four Pillars II: Abstraction
  5. 05Chapter 5: The Four Pillars III: Inheritance
  6. 06Chapter 6: The Four Pillars IV: Polymorphism
  7. 07Chapter 7: Composition vs. Inheritance
  8. 08Chapter 8: Interfaces and Abstract Classes: When to Use Which
  9. 09Chapter 9: SOLID, Part 1: Single Responsibility & Open/Closed
  10. 10Chapter 10: SOLID, Part 2: Liskov Substitution, Interface Segregation, Dependency Inversion
  11. 11Chapter 11: Basic UML for Communicating Design
  12. 12Chapter 12: Anti-Patterns: God Objects, Tight Coupling, Deep Hierarchies, the Fragile Base Class, Anemic Domain Models
  13. 13Chapter 13: OOP Idioms Across TypeScript and Python — and Where OOP Is the Wrong Tool
  14. 14Chapter 14: When OOP Breaks Down: Real-World Failure Modes
  15. 15Chapter 15: Design Exercise: Parking Lot System
  16. 16Chapter 16: Design Exercise: Library Management System
  17. 17Chapter 17: Design Exercise: Elevator Controller
  18. 18Chapter 18: OOP in Interviews: What a Strong Answer Looks Like

Chapter 7 of 18

Chapter 7: Composition vs. Inheritance

Why this matters

"Favor composition over inheritance" is one of the most repeated pieces of OOP advice — and one of the most repeated without understanding why. Chapter 5 showed inheritance's real cost (coupling to a superclass's implementation, the fragile base class problem). This chapter shows the alternative concretely: the same problem, solved both ways, so the trade-off is visible in code rather than asserted as a slogan.

The core concept

[BEGINNER] Composition builds behavior by holding references to other objects (a "has-a" relationship) and delegating to them, instead of inheriting behavior from a superclass (an "is-a" relationship). Where inheritance says "a SavingsAccount is an Account," composition says "a Car has an Engine."

The same problem, solved two ways

Suppose you're building notification-sending behavior that several classes need: OrderService, SignupService, PasswordResetService — each needs to send an email and, for some, also a push notification.

Solution 1 — inheritance:

abstract class NotifyingService {
  protected sendEmail(to: string, subject: string, body: string): void {
    console.log(`Emailing ${to}: ${subject}`);
  }
  protected sendPush(userId: string, message: string): void {
    console.log(`Push to ${userId}: ${message}`);
  }
}

class OrderService extends NotifyingService {
  confirmOrder(userId: string, email: string): void {
    this.sendEmail(email, "Order confirmed", "...");
    this.sendPush(userId, "Your order is confirmed!");
  }
}

This works until a second axis of variation shows up. Suppose PasswordResetService must never send push notifications (security requirement — don't reveal password reset activity outside of email), and a new SmsNotifyingService variant is needed for a service that only has phone numbers. Inheritance forces an awkward choice: keep growing NotifyingService with flags and overrides (sendPush becomes a no-op override in PasswordResetService, which is confusing — the method exists but silently does nothing), or fork the hierarchy. Neither is clean, because the notification channels and the services that use them are two independent things that inheritance has forced into one hierarchy.

Solution 2 — composition:

interface Notifier {
  notify(target: string, message: string): void;
}

class EmailNotifier implements Notifier {
  notify(target: string, message: string): void {
    console.log(`Emailing ${target}: ${message}`);
  }
}

class PushNotifier implements Notifier {
  notify(target: string, message: string): void {
    console.log(`Push to ${target}: ${message}`);
  }
}

class OrderService {
  constructor(private notifiers: Notifier[]) {} // composed, not inherited

  confirmOrder(userId: string, email: string): void {
    for (const notifier of this.notifiers) {
      notifier.notify(email, "Your order is confirmed!");
    }
  }
}

// PasswordResetService simply never receives a PushNotifier — no override, no no-op method, no flag.
const orders = new OrderService([new EmailNotifier(), new PushNotifier()]);
const passwordReset = new OrderService([new EmailNotifier()]);

The notification channels are now independent, swappable, testable pieces. OrderService doesn't inherit a fixed set of notification behaviors — it's handed whichever ones it needs at construction time. Adding SMS support later means writing one new SmsNotifier class; nothing about OrderService or the hierarchy needs to change.

from typing import Protocol

class Notifier(Protocol):
    def notify(self, target: str, message: str) -> None: ...

class EmailNotifier:
    def notify(self, target: str, message: str) -> None:
        print(f"Emailing {target}: {message}")

class PushNotifier:
    def notify(self, target: str, message: str) -> None:
        print(f"Push to {target}: {message}")

class OrderService:
    def __init__(self, notifiers: list[Notifier]) -> None:
        self._notifiers = notifiers

    def confirm_order(self, email: str) -> None:
        for notifier in self._notifiers:
            notifier.notify(email, "Your order is confirmed!")

The decision table

[SENIOR INSIGHT] Neither tool is universally correct. Use this to decide:

QuestionFavors inheritanceFavors composition
Is it a true, substitutable is-a relationship? (Chapter 5, Liskov)YesNo — it's more like has-a or uses-a
Does the behavior vary along more than one independent axis?No — one axis of variationYes — multiple independent axes (as in the notifier example)
Does the relationship need to change at runtime?No — fixed at compile time via class definitionYes — composition can be reconfigured by swapping the held object
Is shared implementation the main goal, more than a type relationship?Sometimes tempting, but often the wrong reason (Chapter 5, mistake #1)Yes — this is exactly composition's strength
How deep would the hierarchy get?Fine for one, maybe two levelsComposition doesn't accumulate hierarchy depth at all

[IN PRACTICE] Most experienced engineers' default is composition, reserving inheritance for the narrow cases that pass the substitutability test cleanly and don't need more than one axis of variation. This is what "favor composition over inheritance" actually means — not "never use inheritance," but "composition should be your first instinct, inheritance the deliberate exception."

Common mistakes

  1. Choosing inheritance because it requires less code up front, without checking whether a second axis of variation is coming. The notifier example's inheritance version looks simpler right up until PasswordResetService's requirement appears.
  2. Building a composition-based design with no interface, just concrete classes held as fields — this loses the flexibility composition is supposed to provide (you can't swap EmailNotifier for a TestNotifier in tests without an interface to substitute against).
  3. Treating "favor composition" as "never use inheritance." Some relationships genuinely are is-a, substitutable, and single-axis — forcing composition there just adds indirection with no benefit (e.g., SavingsAccount extends Account from Chapter 5 is a legitimate use of inheritance).
  4. Mixing the two without a plan — e.g., a deep inheritance hierarchy where each level also composes in unrelated helper objects, producing a design where neither the "is-a" story nor the "has-a" story is coherent.

Checklist: choosing between composition and inheritance

  1. Run the substitutability test first (Chapter 5). If it fails, that alone rules out inheritance.
  2. Count the axes of variation. More than one independent axis (as in notification channel × service type) is a strong signal for composition.
  3. Ask whether the relationship needs to be reconfigurable at runtime. If yes, composition — inheritance is fixed at the class-definition level.
  4. Default to composition when unsure — it's easier to introduce inheritance later for a proven, narrow is-a relationship than to untangle a deep hierarchy after the fact.
  5. When using composition, define an interface for the composed dependency, not just a concrete class — this is what makes composed pieces swappable and testable.

Discussion / interview questions

  1. "Explain 'favor composition over inheritance' — what does it actually mean, and when would you still use inheritance?" Strong answer: Composition should be the default because it avoids implementation coupling and handles multiple independent axes of variation cleanly; inheritance is still right for a narrow, substitutable, single-axis is-a relationship. Not "never use inheritance."

  2. "Walk through a case where inheritance seemed right at first but broke down as requirements grew." Strong answer: Uses a concrete example like the notifier scenario — one axis of variation (notification channel) was fine under inheritance, but a second, independent requirement (a service that must skip one channel) exposed the coupling.

  3. "What does composition give you that inheritance doesn't, in terms of runtime flexibility?" Strong answer: Composed dependencies can be swapped at runtime (different Notifier implementations passed into the constructor); an inherited relationship is fixed once the class is defined and compiled.

  4. "Is 'has-a' always the right signal for composition, and 'is-a' always the right signal for inheritance?" Strong answer: "Is-a" is necessary but not sufficient for inheritance — it must also pass substitutability and ideally be single-axis; the interview shouldn't just pattern-match on the English phrasing of the relationship.

  5. "How would you refactor a class hierarchy that's grown too deep, into a composition-based design?" Strong answer: Identify which behaviors vary independently, extract each into its own interface + implementations, and have the previously-deep subclasses hold references to those interfaces instead of inheriting the behavior — essentially the notifier example's transformation, generalized.

Chapter summary / cheat sheet

  • Composition = has-a, built by holding and delegating to other objects. Inheritance = is-a, built by extending a base class.
  • The same behavior-sharing problem, solved with inheritance, breaks down the moment a second independent axis of variation appears; composition handles that case cleanly by design.
  • Decision signals for inheritance: genuine substitutable is-a relationship, single axis of variation, no need for runtime reconfiguration. Otherwise, default to composition.
  • "Favor composition over inheritance" means composition is the default instinct — not that inheritance should never be used.
  • Composition needs an interface for the composed dependency to deliver its real benefit (swappability, testability) — holding a concrete class as a field isn't enough on its own.