System Design
1. System Prerequisites

Composition vs Inheritance

⏱ 0s active on this topic

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

  1. Tight coupling to the parent's internals. Change Animal.eat() and every subclass can break.
  2. Fragile base class. Adding or changing a parent method can break subclasses in surprising ways, especially with overrides.
  3. You inherit everything — including things you don't want. A Bird extends Animal gets swim() even though it makes no sense.
  4. 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?

QuestionAnswerUse
Is a Dog an Animal?Yes — IS-AInheritance
Is a Manager an Employee?Yes — IS-AInheritance
Does a Car have an Engine?Yes — HAS-AComposition
Does a PaymentService have a Logger?Yes — HAS-AComposition
Is a Stack a Vector?No — a bad IS-AComposition

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

ScenarioUse
Genuine IS-A type hierarchy (Dog is an Animal)Inheritance
Reuse behaviour, no IS-A relationshipComposition
Swap behaviour at runtimeComposition
Framework with defined extension points (BaseController)Inheritance, carefully
Ordinary application/business logicComposition (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 extends just 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 PaymentService extend a Logger class just to reuse log() — it's not a logger, it uses one, so I compose a Logger interface 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's Stack extending Vector is the textbook example of inheritance done wrong."

Follow-ups to expect:

  1. "When IS inheritance the right call?" — True type hierarchies, and framework base classes with genuinely shared behaviour (a BaseController that all controllers extend).
  2. "What's the fragile base class problem?" — Changing or adding a method in a parent silently breaks subclasses, especially ones that override related methods.
  3. "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

  1. Inheritance = IS-A, child gets parent's fields + methods. Composition = HAS-A, class holds objects and delegates.
  2. Favour composition; use inheritance only for a real type hierarchy or framework extension points.
  3. Inheritance downsides: tight coupling to parent internals, fragile base class, forced-to-inherit-everything, deep chains.
  4. Decision test: "Is this truly IS-A, or am I just reusing code?" → reuse = composition.
  5. PaymentService extends Logger is wrong; PaymentService HAS-A Logger (injected interface) is right.
  6. java.util.Stack extends Vector is the famous real-world misuse.

Quick-revision quiz

Question 1 of 17
`PaymentService` needs logging, so a teammate writes `class PaymentService extends Logger`. What's wrong?