Composition vs Inheritance
In simple words
There are two ways for one class to reuse another. Inheritance says one thing is a kind of another: a Dog is an Animal, so a Dog automatically gets everything an Animal has. Composition says one thing has another: a Car has an Engine, so the Car holds an Engine and asks it to do the work. The usual advice is to prefer composition, and to use inheritance only when the 'is a' relationship is truly real.
Inheritance — the child acquires the parent
class Animal {
String name;
void eat() { System.out.println(name + " is eating"); }
void breathe() { System.out.println(name + " is breathing"); }
}
// Dog inherits eat() and breathe(), and adds its own
class Dog extends Animal {
void bark() { System.out.println(name + " says Woof"); }
}
Terms: superclass/parent (inherited from), subclass/child (inherits), extends, super (reference to the parent), override (redefine a parent method), IS-A (the relationship inheritance models).
Overriding — the child changes inherited behaviour
class Animal { String speak() { return "..."; } }
class Dog extends Animal { @Override String speak() { return "Woof"; } }
class Cat extends Animal { @Override String speak() { return "Meow"; } }
Animal a = new Dog();
a.speak(); // "Woof" — the actual object's version runs, not Animal's
That last part is runtime polymorphism — covered in the next topic.
What inheritance gives you
- Code reuse — common logic lives in the parent, not copied into every child.
- A polymorphic type — treat many subtypes through one parent reference.
- A clean model for real "is-a" hierarchies.
Why inheritance gets misused
- Tight coupling to the parent's internals. Change
Animal.eat()and every subclass can break. - Fragile base class. Adding or changing a parent method can break subclasses in surprising ways, especially with overrides.
- You inherit everything — including things you don't want. A
Bird extends Animalgetsswim()even though it makes no sense. - Deep hierarchies become unreadable.
Animal → Vertebrate → Mammal → Carnivore → BigCat → Lion— a change anywhere ripples everywhere.
Composition — the class contains other objects
Instead of Car extends Engine, the car has an engine and asks it to do things:
class Engine { void start() { System.out.println("Engine started"); } }
class Gearbox { void shiftUp() { System.out.println("Shifted up"); } }
class Car {
private final Engine engine = new Engine(); // HAS-A Engine
private final Gearbox gearbox = new Gearbox(); // HAS-A Gearbox
void start() { engine.start(); } // delegate to Engine
void shiftUp() { gearbox.shiftUp(); } // delegate to Gearbox
}
The Car reuses Engine's behaviour without being an Engine, and it can swap the engine for a different one without any of its own code changing (especially if Engine is an interface).
The decision test — IS-A or HAS-A?
| Question | Answer | Use |
|---|---|---|
| Is a Dog an Animal? | Yes — IS-A | Inheritance |
| Is a Manager an Employee? | Yes — IS-A | Inheritance |
| Does a Car have an Engine? | Yes — HAS-A | Composition |
Does a PaymentService have a Logger? | Yes — HAS-A | Composition |
Is a Stack a Vector? | No — a bad IS-A | Composition |
Ask yourself: "Is this genuinely an IS-A relationship, or am I just trying to reuse some code?" If it's the latter, use composition.
Side by side — reusing a Logger
Wrong — inherit just to reuse
class Logger { void log(String m) { System.out.println("[LOG] " + m); } }
class PaymentService extends Logger { // PaymentService IS-NOT-A Logger
void processPayment(double amount) {
log("Processing " + amount);
}
}
PaymentService is now welded to Logger's implementation and can't be given a different logging library.
Right — compose it in
interface Logger { void log(String m); }
class ConsoleLogger implements Logger { public void log(String m) { /* ... */ } }
class FileLogger implements Logger { public void log(String m) { /* ... */ } }
class PaymentService {
private final Logger logger; // HAS-A Logger
PaymentService(Logger logger) { this.logger = logger; }
void processPayment(double amount) {
logger.log("Processing " + amount);
}
}
new PaymentService(new ConsoleLogger());
new PaymentService(new FileLogger()); // swapped with zero changes
The famous mistake — java.util.Stack extends Vector
Because Stack IS-A Vector in the JDK, this compiles and it shouldn't:
Stack<Integer> s = new Stack<>();
s.add(0, 99); // insert at an index — meaningless for a stack
s.remove(2); // remove by index — meaningless for a stack
A Stack should have a list inside and expose only push / pop / peek. Composition would have prevented the leak.
When each one wins
| Scenario | Use |
|---|---|
| Genuine IS-A type hierarchy (Dog is an Animal) | Inheritance |
| Reuse behaviour, no IS-A relationship | Composition |
| Swap behaviour at runtime | Composition |
Framework with defined extension points (BaseController) | Inheritance, carefully |
| Ordinary application/business logic | Composition (default) |
At system-design scale
Almost every "swap the X" statement in a design is composition:
NotificationService HAS-A EmailSender, SmsSender, PushSender → add SlackSender later, no changes
PaymentProcessor HAS-A PaymentGateway (Razorpay / Stripe / Paytm)
ReportService HAS-A DataFetcher + Formatter + Writer → mix and match independently
Where inheritance still fits: framework base classes with genuinely shared behaviour —
class BaseController { void authenticate(Request r) { } void logRequest(Request r) { } }
class UserController extends BaseController { }
class OrderController extends BaseController { }
How This Connects
- Polymorphism (next topic) — the payoff of both: whether you got the type from inheritance or composed an interface, callers work through one reference and the right implementation runs.
- Cohesion & Coupling — inheritance creates tight coupling (child bound to parent internals); composition keeps coupling loose (bound only to a small interface).
- Dependency Injection — composition + "hand the object its collaborators from outside" is exactly DI.
- SOLID — "favour composition" is how you avoid the fragile-base-class trap and keep the Open/Closed and Liskov principles intact.
- Module 3 — HLD — a microservice HAS-A database, HAS-A cache, HAS-A queue. It doesn't extend them.
Trade-offs & common mistakes
- Don't reach for
extendsjust to avoid retyping a few methods — that's the #1 misuse. - Don't build 4+ level inheritance chains; they become impossible to reason about.
- Do keep inheritance for true type hierarchies and framework extension points.
- Note: composition needs a little more wiring (you pass the collaborators in), and heavy delegation can feel verbose. That's a fair price for loose coupling and testability.
In the Interview
When to say it. Any time you're about to write extends to reuse code, or you say "…and I can swap this component":
- "PaymentService has a Logger, it doesn't extend one"
- "the service composes a repository interface, so tests inject a fake"
The ~20-second version:
"I default to composition and only use inheritance for a genuine IS-A hierarchy. The classic misuse: making
PaymentServiceextend aLoggerclass just to reuselog()— it's not a logger, it uses one, so I compose aLoggerinterface in via the constructor. That keeps it loosely coupled — I can swap console for file logging and mock it in tests — and avoids the fragile-base-class problem where a change in the parent breaks every child. Java'sStackextendingVectoris the textbook example of inheritance done wrong."
Follow-ups to expect:
- "When IS inheritance the right call?" — True type hierarchies, and framework base classes with genuinely shared behaviour (a
BaseControllerthat all controllers extend). - "What's the fragile base class problem?" — Changing or adding a method in a parent silently breaks subclasses, especially ones that override related methods.
- "Why does Stack-extends-Vector matter?" — It leaks
add(index)/remove(index)onto a stack, so callers can violate stack semantics. Composition would expose only push/pop/peek.
Mistake to avoid: modelling "shares some code" as "is a". Shared code is a HAS-A (compose it); shared identity is an IS-A (inherit it).
Quick Revision — 6 lines
- Inheritance = IS-A, child gets parent's fields + methods. Composition = HAS-A, class holds objects and delegates.
- Favour composition; use inheritance only for a real type hierarchy or framework extension points.
- Inheritance downsides: tight coupling to parent internals, fragile base class, forced-to-inherit-everything, deep chains.
- Decision test: "Is this truly IS-A, or am I just reusing code?" → reuse = composition.
PaymentService extends Loggeris wrong;PaymentServiceHAS-ALogger(injected interface) is right.java.util.Stack extends Vectoris the famous real-world misuse.