Composition vs Inheritance

Difficulty: Intermediate

Question

What is the difference between composition and inheritance? Why do people say "favour composition over inheritance"?

Answer

This is the question that separates candidates who memorised definitions from those who have thought about design, and it is asked at nearly every product company.

Inheritance says a Dog is an Animal. Composition says a Car has an Engine. In composition, a class holds references to other objects and delegates work to them, rather than inheriting from them. Both are ways to reuse code, but they have very different consequences.

Here is the trouble with inheritance. First, it is a compile-time decision that cannot change: a Dog object can never become a different kind of Animal. Second, the subclass depends on the implementation of its parent, not just its interface. If the parent changes how one method calls another internally, the subclass can silently break. This is the fragile base class problem. The famous example from Effective Java is a HashSet subclass that counts insertions by overriding add and addAll. Since HashSet's addAll internally calls add, each element gets counted twice, and the result is six for three elements. The fix requires knowing the parent's internals, which is exactly what encapsulation was meant to spare us from. Third, inheritance exposes the whole parent API: Stack extends Vector in the JDK lets callers insert in the middle of a stack, which breaks the concept of a stack. Fourth, it leads to deep hierarchies where a change at the top affects everything below.

Composition avoids these issues. You wrap the object, expose only the methods that make sense, and forward calls. The dependency is on the public interface only. You can also swap the component at run time. In the Car example, changing the engine from petrol to electric needs no new Car subclass: you inject a different Engine, which is the basis of the Strategy pattern.

When is inheritance still right? When there is a genuine is-a relationship that satisfies the Liskov Substitution Principle, when the parent is designed for extension and documented that way, and when you actually want polymorphism through a shared type. Even then, favour inheriting from interfaces or abstract classes rather than concrete classes.

A practical rule of thumb: ask if you would ever want to use the subclass anywhere the parent is expected. If not, use composition. Another test: are you inheriting only to reuse a few methods? Then compose.

Also mention aggregation and association as related terms. Association is a general relationship. Aggregation is a has-a where the part can exist independently, like Department and Professor. Composition is a stronger has-a where the part's lifetime is owned by the whole, like House and Room. In UML these have different diamonds, and interviewers sometimes ask for the difference.

In Go and Rust there is no class inheritance at all, and modern Java and Kotlin guidelines increasingly lean towards composition too.

Code examples

Composition allows swapping behaviour at run time

interface Engine { void start(); }

class PetrolEngine implements Engine {
    public void start() { System.out.println("Petrol engine started"); }
}
class ElectricEngine implements Engine {
    public void start() { System.out.println("Electric motor humming"); }
}

class Car {
    private Engine engine;
    Car(Engine engine) { this.engine = engine; }
    void setEngine(Engine engine) { this.engine = engine; }
    void start() { engine.start(); }
}

public class CompositionDemo {
    public static void main(String[] args) {
        Car c = new Car(new PetrolEngine());
        c.start();
        c.setEngine(new ElectricEngine());
        c.start();
    }
}

The fragile base class problem

import java.util.*;

class CountingSet<E> extends HashSet<E> {
    int added = 0;
    @Override public boolean add(E e) { added++; return super.add(e); }
    @Override public boolean addAll(Collection<? extends E> c) {
        added += c.size();
        return super.addAll(c);   // HashSet.addAll calls add() for each element
    }
}

public class FragileBase {
    public static void main(String[] args) {
        CountingSet<String> s = new CountingSet<>();
        s.addAll(List.of("a", "b", "c"));
        System.out.println(s.added);
    }
}

Each element is counted once in addAll and again when addAll calls add internally.

Key points

Concepts covered

composition, inheritance, has-a, fragile-base-class, delegation