The most-repeated advice in design
You will hear “favor composition over inheritance” in nearly every LLD discussion. It’s good advice — but as a default, not a law. Let’s make it concrete.
Two ways to reuse behavior
Inheritance (“is-a”) — a Dog is an Animal, so it inherits Animal’s behavior:
class Animal { breathe() {} }
class Dog extends Animal { bark() {} }Composition (“has-a”) — a Car has an Engine; it holds a reference and delegates:
class Engine { start() {} }
class Car {
private engine = new Engine();
start() { this.engine.start(); } // delegate
}Why composition is usually the safer default
The class-explosion problem
Imagine characters with abilities: walk, fly, swim. With inheritance you end up writing WalkingFlyingCharacter, WalkingSwimmingCharacter, FlyingSwimmingCharacter… every combination becomes a class. With N abilities that’s up to 2ᴺ classes. With composition, you attach the abilities you want to one Character.
Runtime flexibility
Inheritance is fixed at compile time — a Dog can’t stop being an Animal. Composition can change at runtime: a power-up adds a Fly behavior to an existing character. This is exactly what the Strategy pattern formalizes (you’ll meet it in Level 3).
interface MoveBehavior { move(): void; }
class Walk implements MoveBehavior { move() {} }
class Fly implements MoveBehavior { move() {} }
class Character {
private abilities: MoveBehavior[] = [];
addAbility(a: MoveBehavior) { this.abilities.push(a); } // power-up at runtime
act() { this.abilities.forEach(a => a.move()); }
}The fragile base class problem
When many subclasses inherit from one base, a small change to the base can break them in surprising ways. Composition keeps each part independent and separately testable.
So when should you use inheritance?
Inheritance earns its place when all of these hold:
- There’s a genuine “is-a” relationship — not just “I want to reuse this code.”
- The subtype is truly substitutable for the supertype (this is the Liskov Substitution Principle — Level 2).
- The hierarchy is stable and shallow.
A Square that is genuinely a Shape, a SavingsAccount that is genuinely a BankAccount — fine. But “InvoiceService extends DatabaseConnection so it can reuse the query method” is a misuse: that’s a has-a, model it with composition.
The mental test
Before you type extends, ask: “Is this a true is-a, or am I just borrowing code?” If it’s the latter, hold an instance instead of inheriting from it.
In an interview, narrate this tradeoff out loud: “I’ll compose these behaviors rather than inherit, so abilities can change at runtime and I avoid a class explosion.” Interviewers score you on reasoning, not just the final diagram.
The design problem below is the classic that trips up juniors — work it before revealing the reference.