Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
⏱️ 8 min read

Inheritance vs Composition

Inheritance creates an IS-A relationship: a Dog IS-A Animal. Composition creates a HAS-A relationship: a Car HAS-A Engine. The general guidance is β€œfavor composition over inheritance,” but that’s not absolute – inheritance has legitimate uses. Knowing when to use which is what separates good from great OOP design.

Why this matters: In machine coding rounds, reaching for inheritance first often leads to rigid, hard-to-extend hierarchies. But avoiding it entirely means you miss cases where it genuinely simplifies things. Interviewers notice both extremes.


Prerequisites


The Decision Framework

flowchart TD
    A["Is there a genuine IS-A relationship?"]:::client
    B["Would Liskov Substitution hold?"]:::service
    C["Are there fewer than 3 levels?"]:::data
    D["Use Inheritance"]:::service
    E["Use Composition"]:::data
    F["Use Composition"]:::data
    G["Use Composition"]:::data

    A -->|Yes| B
    A -->|No| E
    B -->|Yes| C
    B -->|No| F
    C -->|Yes| D
    C -->|No| G

    classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
    classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
    classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0

When Inheritance Goes Wrong

// PROBLEM: Deep hierarchy with fragile base class
public class Vehicle {
    public void startEngine() { /* ... */ }
    public void accelerate() { /* ... */ }
    public void brake() { /* ... */ }
}

public class Car extends Vehicle {
    public void openTrunk() { /* ... */ }
}

public class ElectricCar extends Car {
    @Override
    public void startEngine() {
        // Awkward: electric cars don't have engines
        // But we inherit startEngine() from Vehicle
        initBattery();
    }
}

public class HybridCar extends Car {
    // Now we need BOTH engine and battery behavior
    // Inheritance can't represent "sometimes gas, sometimes electric"
}

// What about a bicycle? It's a vehicle but has no engine.
// The hierarchy forces us to either:
// 1. Have empty startEngine() implementations (LSP violation)
// 2. Restructure the entire hierarchy
# PROBLEM: class explosion from combining behaviors
class Animal:
    def eat(self): pass

class FlyingAnimal(Animal):
    def fly(self): pass

class SwimmingAnimal(Animal):
    def swim(self): pass

class FlyingSwimmingAnimal(FlyingAnimal, SwimmingAnimal):
    # Diamond inheritance -- which eat() gets called?
    pass

# Want to add running? Now you need:
# RunningAnimal, FlyingRunningAnimal, SwimmingRunningAnimal,
# FlyingSwimmingRunningAnimal... exponential explosion
// PROBLEM: tight coupling to base class internals
class Stack : public vector<int> {
    // Stack inherits ALL of vector's methods
    // Users can call push_back(), insert(), operator[] -- breaking stack semantics
    // This is Java's actual mistake with java.util.Stack extending Vector
public:
    void push(int val) { push_back(val); }
    int pop() {
        int val = back();
        pop_back();
        return val;
    }
    // But nothing stops: stack[3] = 42; or stack.insert(begin(), 99);
};

The Composition Fix

// COMPOSITION: Behaviors as components, assembled freely
public interface PowerSource {
    void start();
    void stop();
    boolean isRunning();
}

public class GasEngine implements PowerSource {
    public void start() { /* ignition */ }
    public void stop() { /* kill engine */ }
    public boolean isRunning() { return ignitionOn; }
}

public class ElectricMotor implements PowerSource {
    public void start() { /* activate motor controller */ }
    public void stop() { /* deactivate */ }
    public boolean isRunning() { return motorActive; }
}

public class HybridPower implements PowerSource {
    private final GasEngine gas;
    private final ElectricMotor electric;
    private PowerMode mode = PowerMode.ELECTRIC;
    
    public void start() {
        if (mode == PowerMode.ELECTRIC) electric.start();
        else gas.start();
    }
    // ...
}

// Vehicle composes its capabilities -- no rigid hierarchy
public class Vehicle {
    private final PowerSource power;
    private final BrakingSystem brakes;
    private final Transmission transmission;

    public Vehicle(PowerSource power, BrakingSystem brakes, Transmission transmission) {
        this.power = power;
        this.brakes = brakes;
        this.transmission = transmission;
    }

    public void start() { power.start(); }
    public void stop() { brakes.apply(); power.stop(); }
}

// Electric car: just different components, same Vehicle class
Vehicle electricCar = new Vehicle(new ElectricMotor(), new RegenerativeBrakes(), new SingleGear());
Vehicle gasCar = new Vehicle(new GasEngine(), new DiscBrakes(), new ManualTransmission());
Vehicle hybrid = new Vehicle(new HybridPower(), new RegenerativeBrakes(), new CVT());
# COMPOSITION: Capabilities as injected components
from abc import ABC, abstractmethod

class MovementCapability(ABC):
    @abstractmethod
    def move(self): ...

class FlyingCapability(MovementCapability):
    def move(self):
        return "flying through the air"

class SwimmingCapability(MovementCapability):
    def move(self):
        return "swimming through water"

class RunningCapability(MovementCapability):
    def move(self):
        return "running on land"

class Animal:
    def __init__(self, name: str, capabilities: list[MovementCapability]):
        self.name = name
        self._capabilities = capabilities
    
    def move(self):
        for cap in self._capabilities:
            print(f"{self.name} is {cap.move()}")

# No inheritance explosion -- just compose capabilities
duck = Animal("Duck", [FlyingCapability(), SwimmingCapability(), RunningCapability()])
fish = Animal("Fish", [SwimmingCapability()])
penguin = Animal("Penguin", [SwimmingCapability(), RunningCapability()])
// COMPOSITION: Stack wraps vector instead of inheriting
template<typename T>
class Stack {
    vector<T> data_; // HAS-A, not IS-A
public:
    void push(T val) { data_.push_back(std::move(val)); }
    
    T pop() {
        if (data_.empty()) throw underflow_error("Stack empty");
        T val = std::move(data_.back());
        data_.pop_back();
        return val;
    }
    
    const T& top() const {
        if (data_.empty()) throw underflow_error("Stack empty");
        return data_.back();
    }
    
    bool empty() const { return data_.empty(); }
    size_t size() const { return data_.size(); }
    
    // No operator[], no insert, no random access -- stack semantics preserved
};

When Inheritance IS Appropriate

Inheritance works well when:

  1. True IS-A with Liskov Substitution: A CheckingAccount IS-A BankAccount and can be used anywhere a BankAccount is expected without surprises.
  2. Shallow hierarchy (2 levels max): Interface -> Concrete implementations.
  3. Template Method pattern: Shared algorithm skeleton with customizable steps.
  4. Framework extension points: HttpServlet, TestCase, Activity – designed for subclassing.
// GOOD inheritance: genuine IS-A, shallow, LSP-safe
public abstract class BankAccount {
    protected Money balance;
    
    public Money getBalance() { return balance; }
    
    public void deposit(Money amount) {
        if (amount.isNegative()) throw new IllegalArgumentException();
        balance = balance.add(amount);
    }
    
    public abstract void withdraw(Money amount);
}

public class CheckingAccount extends BankAccount {
    private final Money overdraftLimit;
    
    @Override
    public void withdraw(Money amount) {
        if (balance.subtract(amount).isLessThan(overdraftLimit.negate())) {
            throw new OverdraftLimitExceededException();
        }
        balance = balance.subtract(amount);
    }
}

public class SavingsAccount extends BankAccount {
    @Override
    public void withdraw(Money amount) {
        if (balance.subtract(amount).isNegative()) {
            throw new InsufficientFundsException();
        }
        balance = balance.subtract(amount);
    }
}
# GOOD: Interface inheritance (defining a contract)
class PaymentProcessor(ABC):
    @abstractmethod
    def charge(self, amount: Decimal, card: Card) -> TransactionId: ...
    
    @abstractmethod
    def refund(self, transaction_id: TransactionId) -> bool: ...

class StripeProcessor(PaymentProcessor):
    def charge(self, amount, card):
        # Stripe-specific implementation
        pass
    
    def refund(self, transaction_id):
        # Stripe-specific refund
        pass

class RazorpayProcessor(PaymentProcessor):
    def charge(self, amount, card):
        pass
    def refund(self, transaction_id):
        pass
// GOOD: Abstract interface with concrete implementations
class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
    virtual double perimeter() const = 0;
    virtual void draw(Canvas& canvas) const = 0;
};

class Circle : public Shape {
    double radius_;
public:
    explicit Circle(double r) : radius_(r) {}
    double area() const override { return M_PI * radius_ * radius_; }
    double perimeter() const override { return 2 * M_PI * radius_; }
    void draw(Canvas& canvas) const override { canvas.drawCircle(radius_); }
};

Quick Reference

Question to Ask If Yes -> If No ->
Is this genuinely IS-A? Consider inheritance Use composition
Would LSP hold for all subtypes? Inheritance is safe Composition
Will the hierarchy stay shallow (2 levels)? Inheritance is manageable Composition
Do subtypes need to combine multiple behaviors? Composition (avoids diamond) Either works
Is behavior swappable at runtime? Composition (inject new behavior) Inheritance is fine
Are you modeling a framework extension point? Inheritance (by design) Composition

When to Use vs When to Avoid

Favor Composition Favor Inheritance
Behaviors need to be mixed and matched True IS-A relationship, LSP holds
You need runtime flexibility Framework explicitly designed for subclassing
Hierarchy would go 3+ levels deep Only 1 level of specialization (interface -> impl)
Multiple independent dimensions of variation Shared algorithm skeleton (Template Method)

Interview Questions

  1. β€œWhy do people say β€˜favor composition over inheritance’?” – Inheritance creates tight coupling to the base class, makes hierarchies rigid, and leads to fragile base class problems. Composition is more flexible and avoids these issues.

  2. β€œGive me a case where inheritance is clearly better.” – Implementing a strategy interface. TokenBucketLimiter implements RateLimitAlgorithm is clean, shallow, and LSP-compliant. You wouldn’t use composition to avoid this.

  3. β€œHow do you refactor from inheritance to composition?” – Extract the varying behavior into an interface. The class that was a subclass now HAS the behavior object instead of being one. Replace extends with a constructor parameter.

  4. β€œWhat’s the diamond problem?” – When a class inherits from two classes that share a common ancestor, ambiguity arises about which parent’s method to use. Composition avoids this entirely.


See It in Action

All LLD problems use this decision. Parking Lot uses composition (strategies), while Vending Machine uses interface inheritance (state objects).

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access