Composition over Inheritance in Java
Learn why has-a relationships outperform is-a for flexible, loosely-coupled Java design.
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
Carhas anEngine, not is anEngine - 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 exists —
Dogis anAnimal, inheritance is appropriate - You need to override parent behavior — inheritance lets you override methods
- Shared immutable state — inheritance with
finalfields 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
| Aspect | Inheritance | Composition |
|---|---|---|
| Coupling | Tight — subclass depends on parent internals | Loose — depends on interface/abstract type |
| Flexibility | Fixed at compile-time | Can swap implementations at runtime |
| Reuse | Inherited code runs in subclass context | Delegated code runs in wrapper context |
| Testing | Hard to mock parent class | Easy to mock collaborator |
| Hierarchy depth | Deep hierarchies problematic | Shallow, 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
- Over-composition — turning everything into small interfaces when a class would suffice
- Missing delegation — forgetting to forward calls to composed objects
- Exposing composed objects — returning internal objects defeats encapsulation
- Wrapper overhead — each wrapper adds a method call (usually negligible)
- 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
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."
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."
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."
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."
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."
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."
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."
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."
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."
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."
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."
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."
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."
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."
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."
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."
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."
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."
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."
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
- Inheritance in Java — when “is-a” relationships are appropriate
- Interfaces in Java — contracts enabling composition
- Polymorphism in Java — polymorphic behavior with composed objects
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.
Arithmetic Operators in Java
Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.
Array Basics in Java
Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.