Skip to content
LLD Learn/OOP Foundations & UML
Browsing as a guest. Sign in to save your progress and earn XP as you complete chapters.

Composition vs. Inheritance

7 min read

You'll learn to

  • -Apply "favor composition over inheritance" with a concrete before/after refactor
  • -Recognize the fragile-base-class and rigid-hierarchy problems inheritance-heavy designs run into during follow-up questions

Tip: the highlighter is on - just select any text below to mark it. Use the highlighter button up top to change color or turn it off. Highlights are saved on this device only. Log in to save them to your account instead.

"Favor composition over inheritance" is one of the most repeated pieces of OOP advice, and also one of the most commonly misapplied - candidates hear it as "never use inheritance," which is wrong. The real guidance is narrower: use inheritance for genuine is-a relationships with stable hierarchies, and use composition (an object holding a reference to another object, and delegating to it) for everything involving behavior that varies or combines independently.

Where Inheritance Breaks Down

Imagine modeling ducks: a Duck base class with a fly() method. Then a RubberDuck needs to exist, and it cannot fly - so you override fly() to do nothing, or throw an exception. Now every piece of code that calls fly() on a Duck has to worry about which subtype it actually got. This is the fragile base class problem: as new variants appear, the hierarchy either grows exception-riddled overrides or forces a redesign, because a single inheritance axis (Duck) cannot cleanly express two independent dimensions of variation (can-fly vs. cannot-fly, quacks-normally vs. squeaks).

Composition fixes what inheritance tangled
# Before: behavior baked into the hierarchy, breaks for RubberDuck
class Duck:
    def fly(self): print("Flying")
    def quack(self): print("Quack")

class RubberDuck(Duck):
    def fly(self): pass  # awkward override - a RubberDuck can't fly

# After: fly and quack behavior are swappable, independent objects
class Duck:
    def __init__(self, fly_behavior, quack_behavior):
        self._fly_behavior = fly_behavior
        self._quack_behavior = quack_behavior

    def perform_fly(self): self._fly_behavior.fly()
    def perform_quack(self): self._quack_behavior.quack()

rubber_duck = Duck(fly_behavior=NoFly(), quack_behavior=Squeak())

When Inheritance Is Still the Right Call

Inheritance still earns its place when the hierarchy is genuinely stable and the is-a relationship is unlikely to need mixing-and-matching later - a CreditCard and DebitCard both being a Payment, for instance, where "how you pay" is not a combination of independent axes. The interview signal is not "did you use inheritance," it is "did you notice when a single hierarchy could not cleanly represent the actual variation in the requirements, and switch to composition instead of forcing it."

A practical smell test: if you find yourself writing an override that does nothing, throws, or exists only to "cancel out" a parent behavior, that is composition's cue to enter, not a sign you need a deeper hierarchy.

Interview Signal

You are designing a notification system with Email, SMS, and Push channels, each of which can optionally be "urgent" (different formatting/delivery). How do you avoid an inheritance explosion (EmailUrgent, SMSUrgent, PushUrgent, ...)?

Weak Answer

"I'd create a base Notification class and have each channel/urgency combination extend it as needed."

Strong Answer

"Channel and urgency are two independent axes of variation, so I'd compose them instead: a Notification holds a DeliveryChannel and a UrgencyFormatter as separate objects rather than baking both into one inheritance chain. That way adding a fourth channel or a second urgency level is one new class, not a multiplication of subclasses."

Check Yourself1 / 3

What problem does the classic Duck/RubberDuck example illustrate?