Composition Over Inheritance in Java – Design Flexible and Maintainable Code

Illustration for Composition Over Inheritance in Java – Design Flexible and Maintainable Code
By Last updated:

Introduction

In object-oriented programming (OOP), inheritance is often the first tool developers reach for—but it's not always the best one.

Composition over inheritance is a principle that promotes flexibility, reusability, and maintainability in software design. In Java, this means favoring "has-a" relationships (composition) over "is-a" relationships (inheritance) when building classes.

In this tutorial, we’ll explore what composition is, how it differs from inheritance, and why it’s a superior choice in many real-world scenarios.


What is Composition?

Composition means building classes using references to other objects, rather than extending classes. A class contains (has-a) another class instead of being (is-a) a subclass of it.

class Engine {
    void start() {
        System.out.println("Engine starting...");
    }
}

class Car {
    private Engine engine = new Engine();

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

Composition vs Inheritance: Key Differences

Feature Inheritance Composition
Relationship Type "is-a" "has-a"
Reusability Through subclassing Through delegation
Flexibility Rigid (tight coupling) Flexible (loose coupling)
Extensibility Hard to change base hierarchy Easy to plug in new behavior
Runtime behavior change Difficult Easy via strategy or delegation

UML-style Example

          Car                             Car
          ↑                               |
        SportsCar                  ----------------
                                     |    Engine   |
                                     ----------------

In inheritance, SportsCar is a Car.
In composition, Car has a Engine.


Java Code Walkthrough: Inheritance

class Animal {
    void speak() {
        System.out.println("Animal speaks");
    }
}

class Dog extends Animal {
    void speak() {
        System.out.println("Dog barks");
    }
}

Problems:

  • Tight coupling to superclass.
  • Cannot change behavior at runtime.
  • Changes in base class ripple through subclasses.

Java Code Walkthrough: Composition

interface Speaker {
    void speak();
}

class Bark implements Speaker {
    public void speak() {
        System.out.println("Dog barks");
    }
}

class Dog {
    private Speaker speaker;

    Dog(Speaker speaker) {
        this.speaker = speaker;
    }

    void makeSound() {
        speaker.speak();
    }
}

Now you can plug in different behaviors without changing the class itself.


Real-World Use Case: Strategy Pattern

interface PaymentStrategy {
    void pay(double amount);
}

class CreditCardPayment implements PaymentStrategy {
    public void pay(double amount) {
        System.out.println("Paid via Credit Card: " + amount);
    }
}

class Order {
    private PaymentStrategy strategy;

    public Order(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    void processPayment(double amount) {
        strategy.pay(amount);
    }
}

You can change PaymentStrategy at runtime — a hallmark of composition.


Pros and Cons

✅ Pros

  • Loose coupling
  • Easier to unit test
  • Behavior can be changed at runtime
  • Avoids fragile base class problems
  • Enables cleaner architecture

❌ Cons

  • Slightly more boilerplate
  • Delegation can sometimes be verbose

Common Misuse Cases

❌ Overusing Inheritance

class Vehicle {
    void move() {}
}

class Airplane extends Vehicle {
    void fly() {} // not all vehicles fly!
}

✅ Fix: Use composition

class Vehicle {
    private Movement movement;

    Vehicle(Movement movement) {
        this.movement = movement;
    }

    void move() {
        movement.perform();
    }
}

  • Inheritance: Code reuse via hierarchy
  • Composition: Code reuse via delegation
  • Interface: Contract abstraction
  • Polymorphism: Behavior flexibility via substitution

Refactoring Example

Before: Inheritance-based Logger

class Logger {
    void log() {}
}

class FileLogger extends Logger {
    void log() {
        System.out.println("Log to file");
    }
}

After: Composition-based Logger

interface LogStrategy {
    void log(String message);
}

class FileLogStrategy implements LogStrategy {
    public void log(String message) {
        System.out.println("File: " + message);
    }
}

class Logger {
    private LogStrategy strategy;

    Logger(LogStrategy strategy) {
        this.strategy = strategy;
    }

    void log(String msg) {
        strategy.log(msg);
    }
}

Java 17/21 Considerations

  • Use records for immutable composite types
  • Combine composition with sealed interfaces for constrained flexibility
record AuthService(UserRepository repo) {
    void login(String username) {
        // delegate to repo
    }
}

Real-World Analogy

Inheritance is like inheriting your parent's house—you can't change its blueprint easily.
Composition is like owning your house and choosing your appliances—you can upgrade parts anytime.


Best Practices

  • Always ask: "Do I really need inheritance?"
  • Favor interfaces + composition for flexibility
  • Use constructor injection to inject behaviors
  • Apply design patterns like Strategy, Decorator, and Adapter
  • Keep classes focused and single-purpose

Conclusion

While inheritance has its place, composition offers a more robust, flexible, and modern way to build software systems in Java. It avoids the pitfalls of deep hierarchies and enables plug-and-play behavior, making your code scalable and easier to maintain.


Key Takeaways

  • Use composition for loose coupling and runtime flexibility
  • Composition promotes clean architecture
  • Inheritance can lead to fragile base class problems
  • Combine composition with design patterns and interfaces
  • Adopt composition early for long-term maintainability

FAQs

1. Is inheritance bad?
No, but it should be used sparingly and only when logical "is-a" relationships exist.

2. Can I combine composition and inheritance?
Yes, but favor composition when possible for greater flexibility.

3. Does composition impact performance?
Negligibly. It’s worth the trade-off for better design.

4. Can static behavior be composed?
Not directly. Static methods belong to classes. Prefer instance-level delegation.

5. Is composition harder to understand?
Initially, yes. But it scales better in large systems.

6. How is composition used in Spring Framework?
Spring encourages composition via dependency injection and interfaces.

7. Does composition support polymorphism?
Yes. You can inject different implementations at runtime.

8. Can I refactor an inheritance hierarchy into composition?
Yes. Replace base class logic with interfaces and delegate behavior.

9. Are records good for composition?
Yes, for immutable data-centric compositions (Java 16+).

10. Which design patterns promote composition?
Strategy, Decorator, Adapter, Bridge—all favor composition.