AlgoInsight

Free lesson · Low Level Design Fundamentals

Why Composition over Inheritance Is Usually Right

Easy10 min readlldoopdesign-patterns

Why "Composition Over Inheritance" Is Usually Right

"Favor composition over inheritance" is one of the oldest and most-repeated guidelines in object-oriented design. It comes from the Gang of Four's Design Patterns (1994), and it has held up through three decades of OO practice. The principle says: when you need to combine behavior or reuse code, prefer holding an object as a field (composition) over extending a class (inheritance).

The guidance isn't absolute - inheritance has legitimate uses, covered in Topic 5.2. But the default lean is dramatic: in most situations, composition is the right answer, and inheritance is the special case. Senior engineers internalize this and reach for composition instinctively. Junior engineers default to inheritance because it's more visible in tutorials and easier to introduce initially.

In interviews, the composition-vs-inheritance choice is silently watched throughout every design problem. Designs that overuse inheritance signal junior thinking; designs that lean on composition signal mature judgment. This topic explains why composition is usually right and what makes inheritance the wrong default.

The Two Approaches Side by Side

Consider a small example: a class that needs both list-like storage and counting behavior.

Inheritance Approach

public class CountingList<E> extends ArrayList<E> {
    private int addCount = 0;
    
    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }
    
    public int addCount() {
        return addCount;
    }
}

CountingList extends ArrayList to get list behavior. It overrides add to count.

Composition Approach

public class CountingList<E> {
    private final List<E> list = new ArrayList<>();
    private int addCount = 0;
    
    public boolean add(E e) {
        addCount++;
        return list.add(e);
    }
    
    public E get(int index) {
        return list.get(index);
    }
    
    public int size() {
        return list.size();
    }
    
    public int addCount() {
        return addCount;
    }
}

CountingList holds an ArrayList as a private field and forwards the methods it wants to expose. It's not a List subclass; it just has-a List.

The two look similar but produce dramatically different behaviors and constraints.

Why Composition Wins - Five Concrete Reasons

Reason 1 - No Fragile Base Class Coupling

The inheritance version has a subtle bug. ArrayList.addAll(Collection) internally calls add(E) for each element. The override counts every add - including those triggered by addAll:

CountingList<String> list = new CountingList<>();
list.addAll(List.of("a", "b", "c"));
System.out.println(list.addCount());   // 6, not 3

Wait, where did 6 come from? Because addAll internally calls add (which counts each), AND the inherited addAll doesn't override the count - so if you'd also overridden addAll, you'd double-count. Either way, the behavior is wrong unless you understand the internal call pattern of ArrayList.

The composition version has no such bug. It doesn't extend anything; it doesn't override anything. Its add is its own method, with no relationship to any internal method calls in ArrayList.

This is the fragile base class problem (Topic 3.1). Inheritance creates implicit dependencies on the superclass's internal behavior. Composition doesn't.

Reason 2 - Selective Exposure

The inheritance version inherits all of ArrayList's public methods, whether you want them or not. Callers can:

CountingList<String> list = new CountingList<>();
list.add("a");
list.add(0, "b");          // inherited from List - doesn't count!
list.set(0, "c");          // inherited from List - doesn't count!
list.remove(0);            // inherited from List - doesn't count!
list.clear();              // inherited from List - doesn't count!

The override of add(E) counts, but every other mutation method is inherited and doesn't count. The counting abstraction is broken by the API surface inherited.

The composition version exposes only what it chooses. If you only expose add and get, that's all callers can use. No accidental access to inherited methods that violate the counting invariant.

Reason 3 - Independent Evolution

When ArrayList changes (a new method is added, internal behavior changes), the inheritance version may need to be updated. The composition version is unaffected unless it specifically uses the changed feature.

This matters in real codebases. A library you depend on releases a new version. Classes inheriting from library types are exposed to behavior changes. Classes composing library types are insulated - they continue to use the library through their narrow exposed interface.

Reason 4 - Flexibility to Change the Backing

The composition version can change its internal storage without affecting callers:

public class CountingList<E> {
    private final List<E> list = new LinkedList<>();   // changed from ArrayList
    // ...
}

The change is invisible to callers. CountingList still has the same public API.

The inheritance version can't easily change its base. To switch from extending ArrayList to extending LinkedList, you must update the class hierarchy - and any code that depends on CountingList instanceof ArrayList breaks.

Reason 5 - Multiple Behaviors

A class can compose multiple other classes. It can only extend one.

public class Order {
    private final List<LineItem> items;
    private final AuditLog audit;
    private final NotificationSender notifications;
    private final PricingEngine pricing;
    // ...
}

Order combines list storage, audit logging, notification, and pricing - four separate concerns, each represented by a dedicated object. With inheritance, Order could extend only one of these, forcing the others to be implemented some other way (typically with more composition or with awkward static helpers).

Performance - A Common Concern

A frequent objection: "composition adds an extra method call. Doesn't that hurt performance?"

In practice, no. The JIT compiler aggressively inlines simple forwarding methods. After a few thousand invocations, the JIT replaces:

public boolean add(E e) {
    addCount++;
    return list.add(e);
}

With effectively:

public boolean add(E e) {
    addCount++;
    // inlined ArrayList.add code
}

The forwarding has no runtime cost in hot code paths. Modern JVMs are very good at this.

In cold code paths, an extra virtual call costs single-digit nanoseconds - far below what's noticeable in any normal application. Reaching for inheritance "for performance" is almost always a premature optimization based on outdated mental models.

The exception: very tight inner loops in performance-critical code (audio processing, game engines, hot paths in databases). Here, every nanosecond counts and the forwarding can matter. But these contexts are rare; for application code, composition has no measurable cost.

When Composition Means More Code

The most legitimate objection to composition: it can require more code, especially for "wrapper" patterns where you want to expose most of the wrapped object's methods.

// Inheritance - one line
public class TimestampedList<E> extends ArrayList<E> {
    public Instant lastModified = Instant.now();
}

// Composition - many forwarding methods
public class TimestampedList<E> {
    private final List<E> list = new ArrayList<>();
    public Instant lastModified = Instant.now();
    
    public boolean add(E e) { lastModified = Instant.now(); return list.add(e); }
    public E get(int i) { return list.get(i); }
    public int size() { return list.size(); }
    public boolean remove(Object o) { lastModified = Instant.now(); return list.remove(o); }
    public Iterator<E> iterator() { return list.iterator(); }
    // ... 20 more forwarding methods
}

The inheritance version is shorter. The composition version is longer because you must forward each method you want to expose.

This is a real cost. In Java, there's no language feature for "forward all methods of this field to my own API," so you write them out manually. (Some languages have this - Kotlin's delegation, Scala's traits with self - but Java doesn't.)

Three ways to mitigate:

1. Expose only what you need. Most wrappers don't need to expose the entire underlying API. Pick the methods that matter.

2. Use the right interface. If you want to expose List behavior, implement List<E> and forward to the internal list:

public class TimestampedList<E> implements List<E> {
    private final List<E> list = new ArrayList<>();
    public Instant lastModified = Instant.now();
    
    @Override
    public boolean add(E e) {
        lastModified = Instant.now();
        return list.add(e);
    }
    
    @Override
    public E get(int i) { return list.get(i); }
    
    // ... forward all List methods
}

The class is still a List<E> from callers' perspective. They can pass it anywhere a List is expected. You write the forwarding methods once.

3. Use IDE generation. Most Java IDEs can generate forwarding methods automatically. The work is mechanical; the IDE handles it.

The cost of writing forwarding methods is real but bounded. It's paid once when the wrapper is created. The benefits - independence from the wrapped class's internals, selective exposure, independent evolution - are paid every day for the wrapper's lifetime.

The Decorator Pattern - Composition's Killer App

The clearest illustration of composition's power is the Decorator pattern (covered in detail in Module 15). It wraps an object with another object that adds behavior:

public interface Notifier {
    void notify(String message);
}

public class EmailNotifier implements Notifier {
    @Override
    public void notify(String message) {
        // send email
    }
}

public class LoggingNotifier implements Notifier {
    private final Notifier wrapped;
    
    public LoggingNotifier(Notifier wrapped) {
        this.wrapped = wrapped;
    }
    
    @Override
    public void notify(String message) {
        System.out.println("Sending: " + message);
        wrapped.notify(message);
    }
}

public class RetryNotifier implements Notifier {
    private final Notifier wrapped;
    
    public RetryNotifier(Notifier wrapped) {
        this.wrapped = wrapped;
    }
    
    @Override
    public void notify(String message) {
        for (int attempt = 0; attempt < 3; attempt++) {
            try {
                wrapped.notify(message);
                return;
            } catch (Exception e) {
                if (attempt == 2) throw e;
            }
        }
    }
}

Each decorator is a small composition wrapper. They stack arbitrarily:

Notifier notifier = new LoggingNotifier(new RetryNotifier(new EmailNotifier()));
notifier.notify("Hello");
// Logs the message, then retries 3x if email fails

Try doing this with inheritance. You'd need LoggingEmailNotifier, RetryEmailNotifier, LoggingRetryEmailNotifier, RetryLoggingEmailNotifier - an explosion of subclasses to represent every combination. Composition handles it in linear classes; inheritance produces combinatorial explosion.

This is one of the strongest practical arguments for composition. The flexibility composition enables would be unwieldy or impossible with inheritance.

Composition + Interfaces - The Strong Pattern

The strongest form combines composition with interfaces. A class implements the interface its callers depend on, while composing the implementation that does the actual work.

public interface OrderRepository {
    Optional<Order> findById(OrderId id);
    void save(Order order);
}

public class CachingOrderRepository implements OrderRepository {
    private final OrderRepository delegate;
    private final Cache<OrderId, Order> cache;
    
    public CachingOrderRepository(OrderRepository delegate, Cache<OrderId, Order> cache) {
        this.delegate = delegate;
        this.cache = cache;
    }
    
    @Override
    public Optional<Order> findById(OrderId id) {
        Order cached = cache.get(id);
        if (cached != null) return Optional.of(cached);
        Optional<Order> found = delegate.findById(id);
        found.ifPresent(o -> cache.put(id, o));
        return found;
    }
    
    @Override
    public void save(Order order) {
        delegate.save(order);
        cache.invalidate(order.id());
    }
}

CachingOrderRepository is an OrderRepository (via the interface), but it doesn't inherit from any specific OrderRepository. It composes a delegate and a cache. It can wrap any OrderRepository - a database-backed one, an HTTP one, a test in-memory one.

The composition + interface pattern is foundational for clean architecture. The interface defines the role; composition assembles behavior into that role. This is the basis for Decorator, Adapter, Proxy, and many other patterns covered in Module 15.

Pros and Cons Side by Side

Composition:
- No fragile base class coupling
- Selective exposure of methods
- Independent evolution from internals
- Multiple behaviors via multiple fields
- Easy to swap implementations
- Decorator pattern enables flexible stacking
- JIT optimizes forwarding to zero cost
- Works with interfaces for clean abstractions

Inheritance:
- Shorter syntax for simple cases
- True is-a relationships expressed naturally
- Polymorphic dispatch through hierarchy
- Framework hooks via overrideable methods
- Sealed hierarchies enable exhaustive pattern matching
- Implicit dependencies on superclass internals
- Tight coupling to superclass evolution
- Inherits all methods, even unwanted ones
- Limited to single-class inheritance in Java

When the Answer Is Still Composition

Even when inheritance seems to fit, composition often serves better. Common cases:

"X is-a Y but only sometimes." If the relationship is conditional, it's not a real is-a. Composition expresses the optionality cleanly.

"X is-a Y but with more state." Adding state to subclasses creates inheritance-coupling problems. Composition expresses the extension naturally.

"X is-a Y now but might become Z later." If the relationship might change, inheritance commits you too early. Composition keeps the option open.

"X is-a Y and a Z." Java's single-class inheritance can't express this. Composition can.

In each case, the right move is to ask whether the relationship is genuinely permanent, exclusive, and identifying. If any of those fails, composition is a better fit.

Why This Matters in Interviews

Interview: The composition-over-inheritance principle is one of the most-watched aspects of LLD interview design:

Implicit signal:

  • Default to composition when designing classes. Senior candidates reach for it instinctively.
  • Justify inheritance when used. "X is-a Y in every case, the hierarchy is stable, we want polymorphic dispatch" is a senior answer. "It seemed easier" is a junior answer.

Direct probes:

  • "Why isn't this inheritance?" - Testing whether you can articulate the composition choice.
  • "What if we wanted to add another behavior?" - Testing flexibility; composition handles this gracefully.
  • "What if the superclass changed?" - Testing fragile-base-class awareness.

Common follow-up probes:

  • "What's the cost of composition here?" - Testing realistic assessment.
  • "How would you handle the wrapper methods?" - Testing knowledge of practical patterns (interfaces, IDE generation).
  • "Could this be Decorator?" - Testing pattern recognition.

A strong articulation during a design probe: "I'll use composition here. The OrderProcessor has-a logger, has-a validator, has-a notifier. None of these are kinds-of OrderProcessor - they're collaborators. Composition expresses the relationship cleanly and lets us swap implementations independently."

Common Mistakes

Warning: Patterns to watch for:

Inheritance for code reuse. "I want to use these methods, so I'll extend the class that has them." Composition or static utilities are the right answer. Covered in Topic 3.6.

Default-to-inheritance habit. Reaching for extends because that's what tutorials show. Pause; ask whether it's really is-a; default to composition if not.

Composition with leakage. Composing an object but exposing it via a getter, defeating the encapsulation. Either compose privately or expose the role interface, not the concrete object.

Premature interface extraction. Adding an interface for the composed object before there's a second implementation. The interface adds indirection without value yet.

Inheritance for "convenience" with shared state. Sometimes a base class with shared state is the right answer; sometimes it's a forced fit. Apply the is-a test honestly.

Try It

Exercise: For each scenario, decide between composition and inheritance, and justify:

  1. A class that needs to log every method call. A LoggingDecorator wrapping the underlying service.

  2. A class that represents a specific subtype of an existing class. SavingsAccount related to Account. Real is-a?

  3. A class that combines behaviors from multiple sources. Audit + Validation + Pricing all needed in Order.

  4. A test stub for a class with complex behavior. Should the stub extend the real class or implement the same interface?

  5. A class that adds caching to an existing repository. Wrap or extend?

  6. A new domain object that has many of the same fields as an existing one. Inheritance or composition?

For each, sketch the design and articulate why you chose composition vs inheritance.

Then audit code you know. Find a place where inheritance was used. Apply the is-a test honestly. If is-a fails, sketch the composition-based alternative. What would change?

Back to Low Level Design Fundamentals