“Favor composition over inheritance” is one of the most quoted principles in software design. But what does it actually mean, and when should we pick one over the other?
Inheritance: The “Is-A” Relationship
Inheritance says: “A Dog is an Animal.” The child class gets everything from the parent.
Animal
└── Dog
└── Cat
This works great for simple hierarchies. But problems creep in when things get deeper or wider.
Composition: The “Has-A” Relationship
Composition says: “A Car has an Engine.” Instead of inheriting behavior, we plug in objects that provide that behavior.
Car
├── has Engine
├── has Transmission
└── has GPS
In simple language, inheritance is about what something IS. Composition is about what something HAS or what it can DO.
The Problem with Deep Inheritance
Let’s say we’re building a game with characters. We start with inheritance:
Character
└── Warrior (can fight)
└── FlyingWarrior (can fight + fly)
└── MagicFlyingWarrior (can fight + fly + cast spells)
Now the designer says: “We need a Mage that can cast spells but can’t fly.” Uh oh. The spell-casting logic is buried inside MagicFlyingWarrior. We’d have to duplicate code or create a weird parallel hierarchy. This is called the diamond problem or fragile base class problem.
Composition to the Rescue
With composition, we break abilities into separate components and mix-and-match:
class FightAbility:
def fight(self):
print("Swinging sword!")
class FlyAbility:
def fly(self):
print("Soaring through the sky!")
class MagicAbility:
def cast_spell(self):
print("Casting fireball!")
class Warrior:
def __init__(self):
self.fighting = FightAbility()
class FlyingWarrior:
def __init__(self):
self.fighting = FightAbility()
self.flying = FlyAbility()
class Mage:
def __init__(self):
self.magic = MagicAbility()
# Mix and match -- no hierarchy headaches
mage = Mage()
mage.magic.cast_spell()
warrior = FlyingWarrior()
warrior.fighting.fight()
warrior.flying.fly()
class FightAbility {
fight() {
console.log("Swinging sword!");
}
}
class FlyAbility {
fly() {
console.log("Soaring through the sky!");
}
}
class MagicAbility {
castSpell() {
console.log("Casting fireball!");
}
}
class Warrior {
constructor() {
this.fighting = new FightAbility();
}
}
class FlyingWarrior {
constructor() {
this.fighting = new FightAbility();
this.flying = new FlyAbility();
}
}
class Mage {
constructor() {
this.magic = new MagicAbility();
}
}
const mage = new Mage();
mage.magic.castSpell();
class FightAbility {
void fight() { System.out.println("Swinging sword!"); }
}
class FlyAbility {
void fly() { System.out.println("Soaring through the sky!"); }
}
class MagicAbility {
void castSpell() { System.out.println("Casting fireball!"); }
}
class Warrior {
FightAbility fighting = new FightAbility();
}
class FlyingWarrior {
FightAbility fighting = new FightAbility();
FlyAbility flying = new FlyAbility();
}
class Mage {
MagicAbility magic = new MagicAbility();
}
// Mage mage = new Mage();
// mage.magic.castSpell();
Now if the designer wants a MagicWarrior, we just create a class with both FightAbility and MagicAbility. No inheritance chain to untangle.
Same Problem, Two Approaches
Let’s see a more realistic example — a notification system:
With inheritance (fragile):
class Notifier:
def send(self, message):
print(f"Email: {message}")
class SMSNotifier(Notifier):
def send(self, message):
print(f"SMS: {message}")
# What if we want BOTH email AND SMS? Uh oh...
# Multiple inheritance? That gets messy fast.
class Notifier {
send(message) {
console.log(`Email: ${message}`);
}
}
class SMSNotifier extends Notifier {
send(message) {
console.log(`SMS: ${message}`);
}
}
// Want both email AND SMS? Can't extend two classes...
class Notifier {
void send(String message) {
System.out.println("Email: " + message);
}
}
class SMSNotifier extends Notifier {
void send(String message) {
System.out.println("SMS: " + message);
}
}
// Java doesn't allow multiple class inheritance.
// Stuck if we want both Email + SMS.
With composition (flexible):
class EmailSender:
def send(self, message):
print(f"Email: {message}")
class SMSSender:
def send(self, message):
print(f"SMS: {message}")
class NotificationService:
def __init__(self, senders):
self.senders = senders # list of sender objects
def notify(self, message):
for sender in self.senders:
sender.send(message)
# Easy -- just plug in what we need
service = NotificationService([EmailSender(), SMSSender()])
service.notify("Your order shipped!")
# Output: Email: Your order shipped!
# SMS: Your order shipped!
class EmailSender {
send(message) {
console.log(`Email: ${message}`);
}
}
class SMSSender {
send(message) {
console.log(`SMS: ${message}`);
}
}
class NotificationService {
constructor(senders) {
this.senders = senders;
}
notify(message) {
this.senders.forEach((s) => s.send(message));
}
}
const service = new NotificationService([
new EmailSender(),
new SMSSender(),
]);
service.notify("Your order shipped!");
interface Sender {
void send(String message);
}
class EmailSender implements Sender {
public void send(String message) {
System.out.println("Email: " + message);
}
}
class SMSSender implements Sender {
public void send(String message) {
System.out.println("SMS: " + message);
}
}
class NotificationService {
List<Sender> senders;
NotificationService(List<Sender> senders) {
this.senders = senders;
}
void notify(String message) {
for (Sender s : senders) s.send(message);
}
}
Want to add Slack notifications later? Just create a SlackSender class and plug it in. No existing code changes.
When to Use Which
| Use Inheritance When… | Use Composition When… |
|---|---|
| There’s a clear “is-a” relationship | Objects need behaviors from multiple sources |
| The hierarchy is shallow (1-2 levels) | We want to swap behaviors at runtime |
| Subclasses truly are specialized versions | The relationship is “has-a” or “can-do” |
| We need to override specific behavior | We want loose coupling |
A good rule of thumb: start with composition. Only reach for inheritance when the “is-a” relationship is obvious and the hierarchy won’t grow beyond 2-3 levels.
In LLD interviews, using composition shows the interviewer we think about flexibility. And that’s exactly what they’re looking for.