Composition over Inheritance
Composition over inheritance naturally aligns with SOLID by keeping responsibilities focused, preserving substitutability, and allowing behavior to evolve independently without breaking existing code.
Inheritance models identity
Composition models capability
- ❌ “All birds can fly” → wrong abstraction
- ✅ “Some birds have flying behavior” → correct abstraction
“Composition over inheritance helps us avoid LSP violations by ensuring classes only expose behaviors they can actually support. Instead of forcing subclasses to override or disable base behavior, we delegate varying behavior to composed components, resulting in more flexible, maintainable, and scalable designs.”
I generally prefer composition over inheritance when behavior varies, because inheritance can easily violate the Liskov Substitution Principle.
Inheritance works well only when there’s a true ‘is-a’ relationship and the behavior is stable across all subclasses. But in real systems, that assumption often breaks down.
A classic example is a Bird base class with a fly() method. Once you introduce a Penguin, you’re forced to override fly() or throw an exception. At that point, the subclass is no longer a valid substitute for the base class, which violates LSP.
Composition solves this by modeling capabilities instead of assumptions. Instead of saying all birds can fly, we say some birds have flying behavior. So we extract flying into a separate behavior and compose it where applicable.
This approach has several benefits:
-
It keeps the design LSP-compliant
-
It reduces tight coupling to base classes
-
It follows Single Responsibility and Open/Closed principles
-
And it allows behavior to change independently, even at runtime
That said, inheritance isn’t bad. I still use it when the abstraction is stable and universally true. But once I see empty overrides, NotImplementedException, or flags controlling behavior, that’s usually a signal to switch to composition.
In short, inheritance models identity, while composition models capability—and capability tends to change more often in real systems.
Below is a holistic, senior-level interview note on “Composition over Inheritance”, structured the way interviewers expect you to think, explain, and apply the concept.
Composition over Inheritance — Senior Software Engineer Interview Notes
1. Definition (Interview-Ready)
Composition over inheritance is a design principle where behavior is built by combining smaller, focused components instead of inheriting behavior from a base class.
The goal is to reduce coupling, increase flexibility, and avoid violating SOLID principles, especially Liskov Substitution Principle (LSP).
In short: “Prefer has-a over is-a when behavior varies.”
2. Why Inheritance Breaks Down at Scale
Inheritance works well only when behavior is stable and universally valid across subclasses.
Typical Problems with Inheritance
-
Subclasses are forced to implement methods they don’t support
-
Requires:
-
Empty overrides
-
Exceptions like UnsupportedOperationException
-
Conditional checks (if (canFly))
-
-
Leads to:
-
LSP violations
-
Tight coupling
-
Fragile base class problem
-
Ripple effects when base class changes
-
These issues usually appear after the system grows, not at the start.
3. Liskov Substitution Principle (Key Link)
LSP states:
Objects of a superclass should be replaceable with objects of its subclasses without breaking correctness.
Inheritance Violation Example
-
Bird has fly()
-
Penguin extends Bird
-
Penguin cannot fly → must override or throw
❌ Penguin is not a valid substitute for Bird
❌ LSP is violated
4. Composition as the Solution
Instead of inheriting behavior, extract varying behavior into separate components.
Design Shift
-
❌ “All birds can fly” → wrong abstraction
-
✅ “Some birds have flying behavior” → correct abstraction
Core Idea
-
Model capabilities, not assumptions
-
Classes delegate behavior to composed objects
5. Conceptual Example
interface FlyBehavior {
fly(): void;
}
class FlyWithWings implements FlyBehavior {
fly() { console.log("Flying"); }
}
class NoFly implements FlyBehavior {
fly() { /* no-op */ }
}
class Bird {
constructor(private flyBehavior: FlyBehavior) {}
fly() {
this.flyBehavior.fly();
}
}
Why This Works
-
Bird doesn’t assume flying
-
Each instance gets appropriate behavior
-
No subclass is forced to lie about its abilities
6. Benefits (Senior-Level Talking Points)
1. LSP Compliance
-
Objects remain safely substitutable
-
No runtime surprises
2. Flexibility
-
Behavior can vary per instance
-
Can change behavior at runtime if needed
3. Better SRP (Single Responsibility)
-
Classes focus on what they are
-
Behaviors focus on what they do
4. Open/Closed Principle
- Add new behavior without modifying existing classes
5. Reduced Coupling
- Changes in one behavior don’t cascade across the hierarchy
7. When to Prefer Composition (Strong Signal Answer)
Use composition when:
-
Behavior differs across subclasses
-
Behavior evolves independently
-
You see:
-
NotImplementedException
-
Boolean flags controlling behavior
-
Large inheritance trees
-
Frequent base-class changes
-
8. When Inheritance Is Still Appropriate
Inheritance is not bad, just easy to misuse.
Use inheritance when:
-
There is a true is-a relationship
-
Behavior is invariant
-
Subclasses fully honor base class contracts
Example: ArrayList is a List
9. Real-World System Design Analogy
Payment System
❌ PaymentProcessor → CreditCardPayment → PayPalPayment
✅ PaymentService has PaymentStrategy
Why?
-
Payment behavior changes frequently
-
New methods added often
-
Composition avoids hierarchy explosion
10. Common Interview Follow-Ups
Q: Is composition always better than inheritance?
A: No. Composition is preferred when behavior varies. Inheritance is fine when the abstraction is stable and universally valid.
Q: Does composition add complexity?
A: Slight upfront complexity, but significantly reduces long-term maintenance cost and design rigidity.
11. One-Minute Interview Answer
“Composition over inheritance helps us avoid LSP violations by ensuring classes only expose behaviors they can actually support. Instead of forcing subclasses to override or disable base behavior, we delegate varying behavior to composed components, resulting in more flexible, maintainable, and scalable designs.”
12. Senior-Level Red Flags to Call Out
-
Deep inheritance trees
-
Base classes changing frequently
-
Subclasses overriding most methods
-
Behavior controlled by flags instead of polymorphism