Composition over Inheritance in Java

Learn why has-a relationships outperform is-a for flexible, loosely-coupled Java design.

published: reading time: 16 min read author: Geek Workbench

Composition over Inheritance in Java

Prefer “has-a” over “is-a”. Instead of inheriting behavior, compose objects with the behaviors you need. This creates flexible, loosely-coupled designs that are easier to test and evolve.

Introduction

The “composition over inheritance” principle recommends “has-a” relationships over “is-a” relationships — objects that contain other objects to obtain behavior, rather than classes that inherit behavior from parent classes. This is not a dismissal of inheritance; it is a response to the specific failure modes that inheritance creates when misused. The fragile base class problem is the core issue: subclasses that depend on parent implementation details break when the parent changes, even when the change seems unrelated to the subclass’s overrides. A concrete Stack extending ArrayList breaks because ArrayList exposes methods (like random access by index) that make no sense for a stack’s LIFO semantics.

Composition solves these problems through delegation. A class depends on an interface contract rather than a concrete implementation. Collaborators are injected via constructors — dependency injection by default — and can be swapped at runtime. This makes testing straightforward: you mock the interface, not the parent class. It also enables patterns that inheritance cannot support at all: runtime behavior addition via the Decorator pattern, where an encryption layer wraps a file writer without the writer knowing it exists; runtime behavior selection via the Strategy pattern, where a pricing engine can swap discount strategies without recompilation.

This post covers the genuine use cases for inheritance (true “is-a” with a stable parent), the patterns composition enables — delegation, Decorator, Strategy — and the failure scenarios that motivate the preference: force-fitting inheritance onto Stack-ArrayList, tight coupling via inheritance chains that break on parent refactoring, and the exposure of inherited methods that violate the contained object’s invariants. By the end, you’ll know when to reach for composition and when inheritance actually fits better.

When to Use

Use composition when:

  • A “has-a” relationship better describes the model — a Car has an Engine, not is an Engine
  • You need behavior from multiple sources — Java single inheritance won’t allow extending multiple classes
  • You want runtime flexibility — swap implementations without changing the class
  • You want loose coupling — classes depend on interfaces, not concrete implementations
  • You need to hide implementation details — wrap and delegate, don’t expose
// Composition: Car HAS-A Engine
public class Engine {
    public void start() { System.out.println("Engine starting"); }
    public void stop() { System.out.println("Engine stopping"); }
}

public class Car {
    private final Engine engine;  // Has-a relationship

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void drive() {
        engine.start();
        System.out.println("Car driving");
        engine.stop();
    }
}

When Not to Use

Don’t use composition when:

  • True “is-a” relationship existsDog is an Animal, inheritance is appropriate
  • You need to override parent behavior — inheritance lets you override methods
  • Shared immutable state — inheritance with final fields may be cleaner
  • Simple use cases — if inheritance is simpler and clear, use it (but be cautious)
// Valid inheritance: Dog truly is an Animal
public class Animal {
    protected String name;
    public void eat() { }
}

public class Dog extends Animal {  // Dog IS an Animal
    private String breed;
    public void bark() { }
}

Dog dog = new Dog();
dog.eat();  // Inherited from Animal — appropriate here

Composition vs Inheritance — Mermaid Diagram

flowchart TD
    subgraph Inheritance
        A1[Animal] --> B1[Dog]
        A1 --> C1[Cat]
        B1 --> D1[Terrier]
    end

    subgraph Composition
        A2[Order] --> B2[Payment]
        A2 --> C2[Inventory]
        A2 --> D2[Shipping]
    end

    style A1
    style A2

Failure Scenarios

1. Force-Fitting Inheritance

// WRONG: Stack is not an ArrayList
public class Stack extends ArrayList {
    public void push(Object item) { add(item); }
    public Object pop() { return remove(size() - 1); }
}

// Problems:
// - ArrayList has methods like get(index), remove(index) that Stack shouldn't have
// - Invariants differ: ArrayList allows any index access, Stack only LIFO
// - Exposes 20+ methods that make no sense for a Stack

// CORRECT: Composition
public class Stack {
    private final List<Object> items = new ArrayList<>();

    public void push(Object item) { items.add(item); }
    public Object pop() {
        if (items.isEmpty()) throw new IllegalStateException("Empty");
        return items.remove(items.size() - 1);
    }
}

2. Fragile Base Class Problem

public class Base {
    public List<String> getItems() {
        return items;  // Returns mutable list — subclasses can break this!
    }

    protected List<String> items = new ArrayList<>();
}

public class Derived extends Base {
    public void addItem(String item) {
        items.add(item);  // Modifies the list from Base
    }
}

// If Base changes to return defensive copy, Derived may break
// If Base changes internals, Derived behavior may change unexpectedly

3. Tight Coupling via Inheritance

// Inheritance creates tight coupling
public class HashMapExtended extends HashMap {
    // If HashMap implementation changes, this class may break
    // Cannot change HashMap behavior, only extend it
    // Cannot easily swap to another map implementation
}

Trade-off Table

AspectInheritanceComposition
CouplingTight — subclass depends on parent internalsLoose — depends on interface/abstract type
FlexibilityFixed at compile-timeCan swap implementations at runtime
ReuseInherited code runs in subclass contextDelegated code runs in wrapper context
TestingHard to mock parent classEasy to mock collaborator
Hierarchy depthDeep hierarchies problematicShallow, flexible structures

Code Snippets

Delegation / Composition with Interface

public interface Logger {
    void log(String message);
}

public class ConsoleLogger implements Logger {
    @Override
    public void log(String message) {
        System.out.println("[CONSOLE] " + message);
    }
}

public class FileLogger implements Logger {
    @Override
    public void log(String message) {
        // Write to file
        System.out.println("[FILE] " + message);
    }
}

public class Service {
    private final Logger logger;  // Composed, not inherited

    public Service(Logger logger) {
        this.logger = logger;
    }

    public void doWork() {
        logger.log("Work started");
        // Do work
        logger.log("Work completed");
    }
}

Decorator Pattern (Runtime Behavior Addition)

public interface DataSource {
    void write(String data);
    String read();
}

public class FileDataSource implements DataSource {
    private final String filename;

    public FileDataSource(String filename) {
        this.filename = filename;
    }

    @Override
    public void write(String data) {
        Files.writeString(Path.of(filename), data);
    }

    @Override
    public String read() {
        return Files.readString(Path.of(filename));
    }
}

// Decorator adds encryption without changing FileDataSource
public class EncryptionDataSource implements DataSource {
    private final DataSource wrapped;

    public EncryptionDataSource(DataSource wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public void write(String data) {
        wrapped.write(encrypt(data));
    }

    @Override
    public String read() {
        return decrypt(wrapped.read());
    }

    private String encrypt(String data) { /* ... */ return data; }
    private String decrypt(String data) { /* ... */ return data; }
}

// Usage — behaviors composed at runtime
DataSource source = new EncryptionDataSource(
    new CompressionDataSource(
        new FileDataSource("data.txt")
    )
);

Strategy Pattern

public interface DiscountStrategy {
    double apply(double price);
}

public class NoDiscount implements DiscountStrategy {
    @Override
    public double apply(double price) { return price; }
}

public class PercentageDiscount implements DiscountStrategy {
    private final double percent;

    public PercentageDiscount(double percent) {
        this.percent = percent;
    }

    @Override
    public double apply(double price) {
        return price * (1 - percent / 100);
    }
}

public class FixedDiscount implements DiscountStrategy {
    private final double amount;

    public FixedDiscount(double amount) {
        this.amount = amount;
    }

    @Override
    public double apply(double price) {
        return Math.max(0, price - amount);
    }
}

public class Product {
    private final String name;
    private final double price;
    private DiscountStrategy discountStrategy;

    public Product(String name, double price, DiscountStrategy discountStrategy) {
        this.name = name;
        this.price = price;
        this.discountStrategy = discountStrategy;
    }

    public double getFinalPrice() {
        return discountStrategy.apply(price);
    }

    public void setDiscountStrategy(DiscountStrategy strategy) {
        this.discountStrategy = strategy;  // Can change at runtime
    }
}

Observability Checklist

  • “Has-a” better describes relationship than “is-a”
  • Dependencies are interfaces or abstract types, not concrete classes
  • Collaborators injected via constructor (dependency injection)
  • Testable — collaborators can be easily mocked
  • Delegation explicit — methods forward to composed objects

Security Notes

  • Dependencies on interfaces — prevents malicious subclass tampering
  • Sealed classes — if inheritance is needed, control which classes can extend (Java 17+)
  • Don’t expose internal collaborators — keep composed objects private
  • Validate injected collaborators — null checks for required dependencies
public class SecureService {
    private final Logger logger;
    private final Validator validator;

    // Constructor injection — dependencies clear and testable
    public SecureService(Logger logger, Validator validator) {
        if (logger == null) throw new IllegalArgumentException("Logger required");
        if (validator == null) throw new IllegalArgumentException("Validator required");
        this.logger = logger;
        this.validator = validator;
    }
}

Pitfalls

  1. Over-composition — turning everything into small interfaces when a class would suffice
  2. Missing delegation — forgetting to forward calls to composed objects
  3. Exposing composed objects — returning internal objects defeats encapsulation
  4. Wrapper overhead — each wrapper adds a method call (usually negligible)
  5. Composition without clear ownership — unclear who owns lifecycle of composed objects
// Bad: composition without delegation
public class Wrapper {
    private final Inner inner;

    public Wrapper(Inner inner) {
        this.inner = inner;
    }

    // WRONG: inner never used — composition without delegation!
    public void doSomething() {
        // Just does its own thing, ignores inner
    }
}

// Good: composition with delegation
public class Wrapper {
    private final Inner inner;

    public Wrapper(Inner inner) {
        this.inner = inner;
    }

    public void doSomething() {
        inner.doSomething();  // Delegates to composed object
    }
}

Quick Recap

  • Composition = “has-a” relationship; objects contain other objects to get behavior
  • Inheritance = “is-a” relationship; subclass automatically gets parent behavior
  • Favor composition when behavior might change, multiple sources of behavior needed, or coupling should be minimized
  • Decorator pattern = runtime addition of behavior via composition
  • Strategy pattern = runtime selection of behavior via composition
  • Delegation = forward calls to composed objects, not inherit their implementation

Interview Questions

1. Why should you prefer composition over inheritance?

Model Answer: "Composition creates looser coupling — classes depend on interfaces, not concrete parent implementations. With composition, you can swap implementations at runtime, test with mocks easily, and avoid the fragile base class problem where parent implementation changes break subclasses. Inheritance works best for true \"is-a\" relationships with stable parent classes."

2. What is the "fragile base class" problem?

Model Answer: "It's when a subclass depends on implementation details of its parent class. When the parent class changes (bug fixes, new methods, internal refactoring), the subclass can unexpectedly break even if it didn't override those specific parts. This is a fundamental flaw of inheritance that composition avoids."

3. How does composition enable runtime flexibility?

Model Answer: "Because composed objects are typically accessed through interfaces, you can swap implementations at runtime without changing the containing class. For example, you can inject a `MockLogger` in tests and a `FileLogger` in production, both satisfying the same `Logger` interface."

4. What is the decorator pattern?

Model Answer: "The decorator pattern wraps an object in another object that adds behavior, without modifying the original class. Each decorator implements the same interface as the wrapped object and delegates calls to it after doing something extra. Decorators can be nested to add multiple behaviors at runtime."

5. When is inheritance actually the right choice?

Model Answer: "Inheritance is appropriate when there is a true \"is-a\" relationship (a `Dog` truly is an `Animal`), when you need to override behavior, when the parent class is stable (won't change unexpectedly), and when the coupling that inheritance creates is acceptable. Simple, shallow hierarchies with immutable parent classes are the safest use case."

6. What is the delegation pattern and how does it relate to composition?

Model Answer: "Delegation is passing method calls to composed objects rather than implementing behavior directly. In composition with delegation, a class contains an interface-typed collaborator and its methods call the collaborator's methods. The Decorator and Strategy patterns both rely on delegation to add or swap behavior."

7. What is the difference between inheritance coupling and composition coupling?

Model Answer: "Inheritance creates tight coupling — a subclass depends on parent implementation details. Composition creates loose coupling — a class depends on an interface contract, not an implementation. When a parent class changes, subclasses can break; when an interface changes, you can manage it with adapters."

8. How does composition enable runtime behavior swapping?

Model Answer: "Composed objects are accessed via interface references, so implementations can be swapped. Dependency injection passes collaborator implementations at construction or via setters. Testing can inject mock implementations; production injects real implementations — the containing class doesn't need to change."

9. What is the relationship between composition and the Strategy pattern?

Model Answer: "The Strategy pattern uses composition to select behavior at runtime. A context class holds an interface reference, and the client can set different strategy implementations. All strategies implement the same interface, so the context doesn't know which strategy it uses — behavior is determined at runtime."

10. Why is composition easier to test than inheritance?

Model Answer: "Composed collaborators are accessed via interfaces, allowing you to inject mock implementations. Inheritance makes testing harder because a subclass is tightly bound to parent implementation. With composition, you test the class in isolation by mocking its dependencies rather than needing the actual parent class."

11. What is the difference between composition and aggregation?

Model Answer: "In composition, the contained object's lifetime matches the container's — the container owns the parts. In aggregation, the contained object can exist independently — it has a separate lifetime. Both use \"has-a\" relationships; the difference is ownership and lifecycle semantics."

12. How does composition support the Interface Segregation Principle?

Model Answer: "ISP states that classes should not be forced to depend on methods they don't use. Composition allows depending on small, focused interfaces rather than large ones. A class requiring only `Readable` can compose with that rather than depending on an entire `FileHandler` interface."

13. What is the fragile base class problem and why does composition avoid it?

Model Answer: "The fragile base class problem occurs when changes to a parent class break subclass behavior unexpectedly because subclasses depend on parent implementation details, not just the public contract. Composition avoids it because a class depends on an interface contract — implementation changes don't affect consumers as long as the interface is maintained."

14. When using composition, how do you decide what interfaces to create?

Model Answer: "Create interfaces around the behavior your class needs from collaborators. Follow Single Responsibility: each interface represents one role or capability. Name interfaces by what they allow (Readable, Writable, Serializable) rather than by what implements them."

15. How does composition relate to dependency injection?

Model Answer: "Dependency injection passes dependencies (composed objects) from outside rather than creating them internally. Constructor injection declares dependencies as interface-typed constructor parameters. This makes code more testable and flexible — dependencies can be swapped at construction time."

16. What is the difference between composition and inheritance for code reuse?

Model Answer: "Inheritance: code is \"inherited\" into a subclass — runs in subclass context with direct access to parent state. Composition: code is \"delegated\" to a collaborator — the collaborator runs its own code and the class wraps the result. Composition reuse is via delegation; inheritance reuse is via code copying into subclasses."

17. Can composition and inheritance be used together?

Model Answer: "Yes — a class can extend one class (inheritance) and compose with interfaces (composition). A common pattern is to extend a `BaseClass` and implement several interface-typed collaborators. This gives shared implementation from the parent plus flexible behavior from composition."

18. What is the "composition over inheritance" principle's impact on class hierarchies?

Model Answer: "It reduces deep inheritance hierarchies by replacing \"is-a\" chains with \"has-a\" collaborations. This results in flatter hierarchies with more interfaces and fewer parent-child dependencies. The outcome is more composable, flexible code that can adapt to changing requirements."

19. What is the difference between forwarding and delegation in composition?

Model Answer: "Forwarding is when a wrapper delegates to a collaborator without adding any behavior of its own. Delegation is when a wrapper may add behavior before or after calling the collaborator's method. The Decorator pattern is delegation; a simple wrapper interface is forwarding."

20. How does composition help avoid the "diamond problem" in Java?

Model Answer: "Java doesn't support multiple class inheritance, so the diamond problem is avoided for state. Composition avoids it by using interfaces without state — there is no ambiguity in method resolution. Interface default methods can still cause diamond issues, but only for behavior, not state inheritance."

Further Reading

Conclusion

Composition uses “has-a” relationships where objects contain other objects to obtain behavior, favoring delegation over code inheritance. It creates loose coupling through interface dependencies, enabling runtime behavior swapping, easier testing with mocks, and avoidance of the fragile base class problem where parent changes unexpectedly break subclasses. Decorator and Strategy patterns are classic composition patterns that add or select behavior at runtime.

Prefer composition when behavior might change, multiple sources of behavior are needed, or coupling should be minimized. Use inheritance for true “is-a” relationships with stable parent classes where the coupling is acceptable and overriding behavior is needed. The golden rule: if a class needs to use functionality from another class, ask whether it “is-a” that class or “has-a” that class — composition wins in most scenarios.

This principle directly addresses the risks of inheritance by providing an alternative that avoids the tight coupling and fragile hierarchies that inheritance can create.

Category

Related Posts

Abstract Classes in Java

Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.

#java-abstract-classes #java #java-fundamentals

Arithmetic Operators in Java

Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.

#java-arithmetic-operators #java #java-fundamentals

Array Basics in Java

Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.

#java-array-basics #java #java-fundamentals