Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Favor Composition
LLD

Favor Composition

Understand why composing objects is often more flexible and maintainable than relying heavily on inheritance.

The Problem It Solves

Behavior needs combining: an audit-logged repository, a cached one, a retried one — eventually all three. Inheritance forces combinations into the type tree at compile time (LoggedCachedRetryingRepo), one class per combination, each inheriting everything from a chain it cannot opt out of. Composition assembles behaviors per instance at runtime, leaving classes single-purpose. The principle exists because inheritance couples you to a fixed hierarchy; composition lets behavior be data.

The Duck That Motivated Every Textbook

Requirements: ducks swim; some fly; some quack. First change: “rubber ducks squeak.” Second: “decoy ducks are silent.” Third: “some ducks fly with jetpacks.” Every variation lands in Duck’s inheritance tree, and sibling fixes leak to cousins.

 INHERITANCE TREE                          COMPOSITION WIRING

        Duck (fly(), quack())              Duck ──► FlyBehavior   (interface)
       /   |   \                                  ──► QuackBehavior
   Rubber Decoy Mallard                        FlyWithWings | FlyNoWay | JetPack
   squeak   mute   quack                       Squeak | Mute | Quack
   + jetpack duck = NEW subclass               jetpack = inject JetPack at runtime
     for every cousin combination               per-instance, no new types

Left: combinations multiply as subclasses. Right: two small interface families; any duck gets any pairing injected in its constructor — combinations become configuration, not class explosion.

Mechanics

class Duck {
    private final FlyBehavior fly;      // HAS-A capabilities
    private final QuackBehavior quack;
    Duck(FlyBehavior f, QuackBehavior q) { this.fly = f; this.quack = q; }
    void performFly() { fly.fly(); }    // delegated, not inherited
}
new Duck(new JetPackFly(), new MuteQuack());

Why It Wins

ConcernInheritanceComposition
Combining N behaviorsOne class per combinationInjected per instance
Changing behavior at runtimeImpossibleSwap the field
Encapsulation of delegate internalsBroken (white-box)Intact (black-box)
Base-class evolution riskFragile-base breakageNone — only public API known
TestingConstruct real superclass stateInject fakes

When Inheritance Still Wins

  • Genuine is-a identity with LSP-safe substitution (FileNotFoundException extends IOException) — polymorphism over shared identity is inheritance’s irreplaceable job.
  • Framework extension points designed for it (template methods: HttpServlet, AbstractList).
  • Deep code reuse across a stable, documented hierarchy.

The principle says favor, not never: reach for composition when behavior varies or combines; keep inheritance for true taxonomy.

Interview Signals

  • Answering “how would you add X behavior?” with injection rather than a new subclass is the discriminator.
  • Citing the cost honestly — delegates need forwarding boilerplate (Java lacks language support; Kotlin/Lombok reduce it) — reads as experience.

My Private Notes

Notes are auto-saved locally to this device.