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
| Concern | Inheritance | Composition |
|---|---|---|
| Combining N behaviors | One class per combination | Injected per instance |
| Changing behavior at runtime | Impossible | Swap the field |
| Encapsulation of delegate internals | Broken (white-box) | Intact (black-box) |
| Base-class evolution risk | Fragile-base breakage | None — only public API known |
| Testing | Construct real superclass state | Inject 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.
Premium Content
Unlock Favor Composition and all premium lessons with a subscription.
From ₹199.99/year — See plans