Skip to content

Composition over Inheritance#

classDiagram
  direction LR
  class Robot {
    -mover: Mover
    -sensor: Sensor
    -gripper: Gripper
    +act()
  }
  class Mover
  class Sensor
  class Gripper
  Robot o--> Mover
  Robot o--> Sensor
  Robot o--> Gripper

Prefer giving a class collaborators (composition) over making it a subtype of something. Inheritance commits you to a hierarchy; composition gives you a swappable bag of behaviours.

The inheritance trap#

classDiagram
  class Animal {
    +eat()
    +sleep()
  }
  class Bird {
    +fly()
    +eat()
    +sleep()
  }
  class Fish {
    +swim()
    +eat()
    +sleep()
  }
  class Penguin {
    +eat()
    +sleep()
    +swim()
    +fly()  -- throws UnsupportedOperationException
  }
  Animal <|-- Bird
  Animal <|-- Fish
  Bird <|-- Penguin

A Penguin is a Bird but can't fly. Either: - override fly() to throw (Liskov-violating); or - restructure the hierarchy (and rebuild your codebase).

The composition fix#

classDiagram
  direction LR
  class Animal {
    -mover: MoveBehavior
    +move()
  }
  class MoveBehavior {
    <<interface>>
    +move()
  }
  class Fly
  class Swim
  class Walk
  MoveBehavior <|.. Fly
  MoveBehavior <|.. Swim
  MoveBehavior <|.. Walk
  Animal o--> MoveBehavior

Each animal is composed of what it can do. A penguin gets Swim + Walk, no Fly. New behaviour = new class, no hierarchy edits.

Rules of thumb#

  • "is-a" → inheritance (rare).
  • "has-a" / "can do" → composition (default).
  • If you're inheriting only to share code, prefer composition + delegation.
  • If you're inheriting to be the parent type in the type system, inheritance is fine - but watch LSP.

Mixin / trait alternatives#

Languages like Scala (traits), Rust (impl blocks), TypeScript (intersection types), Python (mixins) let you mix in capabilities without single-line inheritance.

Where this shows up in this site#

  • Chess engine - Piece doesn't subclass MoveStrategy; it has a move strategy.
  • Notification system - Notification doesn't extend EmailDeliverer; it composes a Channel.
  • Logger framework - Logger composes appenders + formatters instead of inheriting from them.
  • Vending machine / Hotel management - pricing, dispatch, and state machines composed in.

Glossary & fundamentals#

Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.

Tag Concept What it is Page
HLD Observability metrics, logs, traces, SLOs observability
LLD State machines FSM, HSM, transitions, guards state-machines
LLD Async models futures / async-await / coroutines / actors async-models
LLD OOP pillars encapsulation, abstraction, inheritance, polymorphism oop-pillars
LLD SOLID principles SRP / OCP / LSP / ISP / DIP solid-principles
LLD Behavioural patterns Strategy, Observer, State, Command, Chain behavioral-patterns
LLD Composition over inheritance prefer composition; small swappable parts composition-over-inheritance
LLD Error handling exceptions vs Result, error boundaries error-handling

Quick reference#

Why it usually wins#

  • Behaviours can be swapped at runtime.
  • Easier to test (mock the collaborator).
  • Avoids deep hierarchies (which are hard to reason about).
  • Sidesteps multiple-inheritance issues.

When inheritance is fine#

  • Closed, stable hierarchies (Shape → Circle/Rect/Triangle).
  • Templates and frameworks that explicitly require it (Spring's HandlerAdapter, Android Activity).
  • Trivial extension where overriding is the natural mechanism.

"Favor composition" ≠ "never inherit"#

  • Single-level inheritance is often perfectly fine.
  • Inheritance chains > 2 levels are a smell.
  • "Marker interfaces" or sealed hierarchies model algebraic data types cleanly.

Refs#

  • Effective Java - Items 18 (favor composition over inheritance), 19 (design for inheritance or prohibit it).
  • Design Patterns (GoF) - many patterns are composition-based alternatives to inheritance.
  • Go: design and evolution talks - Go intentionally drops inheritance.

FAQ#

Why prefer composition over inheritance?#

Composition keeps behavior swappable at runtime and avoids the fragile base class problem. Inheritance hard-codes relationships at compile time and tends to leak parent details into subclasses.

When is inheritance still the right choice?#

When you have a true is-a relationship with a stable hierarchy and want to share invariants, like AbstractList in Java collections. Even then, prefer interfaces plus composition for behavior.

What is the fragile base class problem?#

Changes to a base class can silently break subclasses that depended on subtle behavior. Composition avoids this because collaborators only see public interfaces, not protected internals.

How does the Strategy pattern apply composition?#

Instead of subclassing to vary an algorithm, the class holds a Strategy collaborator that can be swapped at runtime. New behaviors become new strategy classes, not new subclasses.

Does composition cost more boilerplate?#

Yes, you write more constructors and interfaces. Modern languages with records, lambdas, or dependency injection greatly reduce this overhead and make composition the default choice.

  • SOLID Principles: OCP and DIP directly motivate composition over inheritance
  • Structural Patterns: Decorator and Bridge are the key structural patterns that replace inheritance hierarchies
  • Behavioral Patterns: Strategy and Command patterns illustrate composition in action

Further reading#

Curated, high-credibility sources for going deeper on this topic.