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();
}
}
Comparison with Related Concepts
- 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.