Composition vs Inheritance: choosing the right relationship
Inheritance models IS-A but couples tightly. Composition models HAS-A and stays flexible. Learn when each is right and how to migrate from one to the other.
Introduction
Choosing between composition and inheritance is a boundary decision: decide whether the new type should be substitutable for an existing type, or whether it should collaborate with one. The examples below use Java, but the model applies to object-oriented designs in general.
TL;DR / Mental Model
- Inheritance means IS-A: a subtype must honor the parent contract everywhere the parent is expected.
- Composition means HAS-A/USES: a class owns or collaborates with another object and exposes only the API it intends to support.
- Start with substitutability and lifecycle. Choose composition when you need to combine behaviours, swap implementations, or protect callers from an oversized API.
The Problem
A common inheritance mistake in interviews is inheriting for reuse, not for IS-A. Consider a developer who needs LoggingService to send logs over HTTP. They notice HttpClient already has get() and post(), so they extend it.
// β LoggingService IS-A HttpClient? No. It USES an HttpClient.
// This inherits the full HttpClient API: get(), post(), put(), delete(), setHeaders()...
// Every caller of LoggingService can now make arbitrary HTTP calls through it.
public class LoggingService extends HttpClient {
public void log(String level, String message) {
// reusing the parent's post() -- but this couples the service
// to HttpClient throughout the inheritance relationship
post("/logs", buildPayload(level, message));
}
}
This design creates three problems. Callers can call loggingService.delete("/users/123") and it compiles. Switching from HttpClient to WebClient can require changes to LoggingService and its subclasses. Unit tests for LoggingService may need an HTTP server or complex mocking of superclass internals.
Here is what changes when you apply composition instead.
Core Concept
Inheritance (extends) says: "This class IS a kind of the parent class." The subclass picks up all parent fields, methods, and implementation details. This is appropriate when the relationship is genuinely substitutable (see Liskov Substitution Principle).
Composition (has-a field) says: "This class USES another class to do part of its job." The outer class controls the interface it exposes and delegates internally. You can swap the inner class, mock it, or change it without touching callers.
Definitions and boundaries
The boundary is substitutability, not code reuse. If callers can use an instance of the subtype anywhere the parent is expected without weaker guarantees or surprising behaviour, inheritance may be appropriate. If the new class only needs selected operations, keep the collaborator behind a field and expose a smaller API.
In this article, composition means assembling behaviour from collaborators. That field may represent exclusive lifecycle ownership, a shared association, or a strategy supplied by dependency injection; UML's filled diamond is the narrower lifecycle-specific form described in the composition article.
Logger holds a LogFormatter and a LogWriter as fields. At construction time you inject any combination: JsonFormatter with FileWriter, PlainTextFormatter with HttpWriter, or a mock for tests. The formatter and writer hierarchies are independently extensible. A new DatabaseWriter can often be added without modifying Logger or existing formatter implementations.
Implementation
// Strategy interface for formatting.
// Logger never knows whether the output is JSON, plain text, or XML.
// Adding a new format is a new class, not a new branch in Logger.
public interface LogFormatter {
String format(String level, String message);
}The broken version (LoggingService extends HttpClient) makes tests depend on HTTP behaviour or superclass mocking. The composed version lets you pass a lambda as LogWriter in tests because LogWriter is a single-method interface. That is the composition payoff.
How It Works at Runtime
- The application wires
Loggerwith specific implementations at startup (or via DI container). Logger.log()callsformatter.format()without knowing which formatter it is.- The formatted string is handed to
writer.write()without knowing the destination. - To swap the format, you pass a different
LogFormatterimplementation.Loggeris untouched. - This is the "swap at construction time" benefit of composition. With inheritance, representing the same variation generally requires another subtype or a hierarchy change.
Common Pitfalls
| Anti-Pattern | What It Looks Like | Why It Hurts |
|---|---|---|
| Inheriting for reuse | LoggingService extends HttpClient to reuse post() | Leaks all 20 parent methods to callers; switching away requires redesign |
| Deep hierarchies | A extends B extends C extends D | Any change in B can affect C and D; the hierarchy is harder to reason about |
| Overriding to neutralize | Overriding a parent method to throw UnsupportedOperationException | Violates LSP; callers can't substitute the subclass for the parent safely |
| Premature abstraction | Writing a base class before you have two real subclasses | You guess wrong, then refactor the hierarchy twice |
| Composition dumping | Injecting 8 collaborators into one constructor | The class has too many responsibilities; split the class before fixing the wiring |
Common misconceptions
- Composition is not automatically better. It adds delegation and wiring, so use it when the collaborator boundary represents a real variation or ownership decision.
- An interface does not remove coupling; it moves the coupling to a smaller, more stable contract.
- Inheritance is not forbidden. Framework extension points, Template Method, and a stable subtype hierarchy can make it a reasonable choice.
When to Use Each
Use this table when the flowchart is too abstract:
| Situation | Pick |
|---|---|
| Reusing implementation from another class | Composition |
Extending a framework base class (AbstractList, HttpServlet) | Inheritance |
| Need to swap behaviour at runtime or in tests | Composition |
| Adding behaviour to a closed class you cannot modify | Decorator (composition pattern) |
| True IS-A with stable hierarchy and LSP compliance | Inheritance is OK |
| Algorithm skeleton shared across variants (Template Method) | Inheritance inside the pattern only |
Design Implications and Trade-offs
Composition narrows the public surface of the outer class, allows collaborators to vary independently, and usually makes seams for testing explicit. The costs are extra objects, constructor wiring, and a possible layer of delegation that can make control flow less obvious. Inheritance can provide a useful shared algorithm or framework hook, but it exposes the parent contract and couples subclasses to base-class evolution.
Treat the decision as contextual rather than as a rule with no exceptions:
- Use inheritance when the subtype truly preserves the parent contract and the hierarchy is stable enough to own that coupling.
- Use composition when behaviour should be combined, replaced, decorated, or configured independently.
- Keep lifecycle ownership separate from the choice between inheritance and ordinary delegation. A composed collaborator may be shared or independently managed; only the domain and lifecycle semantics justify a UML composition diamond.
Real-World Examples
Java I/O Streams use composition, not inheritance, for layering behaviour. BufferedInputStream holds a reference to another InputStream (composition field), not an extension of one. You stack behaviours: new GZIPInputStream(new BufferedInputStream(new FileInputStream(path))). Each wrapper composes the one below.
Spring Framework uses composition for dependency injection. Your OrderService does not extend AbstractOrderService. It receives OrderRepository, NotificationService, and InvoiceService as constructor arguments. The Spring container wires them. This is a large-scale example of composition.
Go has no class inheritance. It uses interface satisfaction for behaviour contracts and struct fields or embedding for reuse and composition. This illustrates that many designs can model polymorphism without a class hierarchy.
Java Comparator is a composition example in the standard library. Instead of SubclassedList extends ArrayList with custom sort logic, you pass a Comparator (a composed behaviour) to Collections.sort(). The sort algorithm is composed with the comparison strategy at call time.
Interview Tips
Most common mistake
"I used inheritance to reuse the save() method." Reuse alone is not sufficient justification for inheritance. It is a valid reason to consider composition. Effective Java Item 18 recommends composition over inheritance for reuse.
Mistake 1: Deep hierarchy as a sign of good design Candidates sometimes say "I have a 5-level hierarchy" as a positive. Treat that as a warning sign: every level of inheritance adds another coupling point. Beyond 3 levels, ask whether composition would express the variation more clearly.
Mistake 2: Confusing IS-A relationship with implementation similarity
Two classes can look similar and still not have an IS-A relationship. Logger and HttpClient both make network calls. That is not IS-A. It is "coincidentally similar". The test is substitutability: can you pass a Logger wherever an HttpClient is expected and have it make sense? No. So no inheritance.
Mistake 3: Not knowing the "fragile base class" problem A parent class change breaks a subclass in a non-obvious way. Classic interview answer: "I'd seal the base class" or "I'd use composition so there is no base class to be fragile."
Mistake 4: Overriding to neutralize
// β If you're doing this, you should be using composition
@Override
public void dangerousParentMethod() {
throw new UnsupportedOperationException("Not supported");
}
This is an LSP violation. Callers relying on the parent's contract get a runtime exception instead.
A useful shorthand: "Favor composition over inheritance" appears in both Effective Java (Item 18) and the original GoF Design Patterns book. The underlying reasons are tight coupling and the fragile-base-class problem; apply the rule through substitutability and lifecycle rather than quoting it mechanically.
30-Second Explanation
Inheritance says a subtype is a kind of its parent, so it must remain substitutable for that parent. Composition says a class uses collaborators and chooses the small interface it exposes. Prefer composition for reuse, swappable strategies, and decorators; use inheritance when the IS-A relationship and the parent contract are genuinely stable, including a deliberate framework or Template Method extension.
5-Minute Explanation
- Start with the contract: can the proposed subtype be passed anywhere the parent is expected without surprising callers? If not, do not inherit.
- Separate reuse from type identity. Reuse a formatter, writer, repository, or policy by storing it behind a field and delegating to it.
- Model variation as collaborators. In the logger example,
LogFormatterandLogWritervary independently, so oneLoggercan combine them without a subclass for every pair. - Check the trade-off. Composition adds wiring and indirection; inheritance can simplify a stable algorithm skeleton but exposes parent methods, state, and future changes.
- Check the exception. Framework-controlled extension points and Template Method can justify inheritance, while a decorator or strategy usually keeps the variation in composition.
Test Your Understanding
Answer these short checks in one sentence before opening the detailed cases below.
Quick Checks
- Question: What is the first test for inheritance? Answer: Whether the subtype is safely substitutable for the parent.
- Question: What does composition buy the
Loggerexample? Answer: Independent formatter/writer variation behind a narrow API. - Question: Is a UML filled diamond required for every composed collaborator? Answer: No; it specifically denotes exclusive whole-part lifecycle ownership.
- Question: Name one legitimate inheritance exception. Answer: A framework extension point, Template Method, or stable LSP-compliant subtype.
Quick Recap
- Inherit only for IS-A relationships that pass the LSP substitutability test. Reuse alone is not enough justification.
- Composition gives you runtime flexibility: swap implementations via constructor injection without modifying the class.
- The fragile base class problem is real: parent changes silently break subclasses in ways the compiler cannot catch.
- Deep hierarchies (beyond 3 levels) deserve scrutiny: each level is another coupling point that can make changes harder to reason about.
- Decorator is composition for behaviour: wrap a class you can't modify instead of extending it.
- Go's model illustrates the alternative: interface contracts and struct composition can express polymorphism without class inheritance.
- In interviews: explain that you reach for composition first and use inheritance when the IS-A relationship is genuine and LSP-compliant; Effective Java Item 18 is a useful reference.
Related OOP Concepts
- Liskov Substitution Principle: the contract test that makes inheritance safe.
- Encapsulation: composition lets the outer class hide collaborator details and expose a focused API.
- Strategy and Decorator: common patterns for composing replaceable or additional behaviour.
- Aggregation and association: related has-a relationships distinguished by sharing and lifecycle ownership.
- Dependency Inversion: depend on stable abstractions when a collaborator needs to vary.
Related Articles
Inheritance models IS-A relationships and enables polymorphism, but it creates tight coupling. Use composition when in doubt.
Abstraction separates what an object can do from how it does it. Callers depend on contracts, not implementations, which makes systems extensible and independently testable.