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.
The Problem
The most common inheritance mistake I see in interviews is inheriting for reuse, not for IS-A. Here is a real example: a developer needs LoggingService to send logs over HTTP. They notice HttpClient already has get() and post(), so they just 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 parent's post() -- but this couples us to HttpClient forever
post("/logs", buildPayload(level, message));
}
}
The moment you ship this, three things go wrong. Callers can call loggingService.delete("/users/123") and it compiles. Switching from HttpClient to WebClient requires rewriting LoggingService and every subclass of it. Unit tests for LoggingService 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.
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. Adding a DatabaseWriter touches zero existing classes.
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) forces every test to mock HTTP. 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. Inheritance cannot do this without rewriting the class hierarchy.
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; inheritance cannot be undone at runtime |
| Deep hierarchies | A extends B extends C extends D | Any change in B potentially breaks C and D; impossible 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 |
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 |
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 industrial-scale composition.
Go's entire type system has no inheritance. Every "is-a" relationship is expressed through interface satisfaction. Every "has-a" relationship is expressed through struct embedding (which is composition, not inheritance: the outer struct holds the inner struct). Go's conclusion after 15 years of production use at Google: you almost never need inheritance.
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 never a valid reason for inheritance. It is a valid reason for composition. Effective Java Item 18 says this explicitly: prefer 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. Red flag. Every level of inheritance adds coupling. Beyond 3 levels I start asking "why isn't this composition?"
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.
The interview quote to know: "Favor composition over inheritance" appears in both Effective Java (Item 18) and the original GoF Design Patterns book. Every senior engineer interview will touch this. Know the phrase and the two reasons: tight coupling and fragile base class.
Test Your Understanding
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) are a red flag: each level is another coupling point waiting to cause a problem at 2 AM.
- Decorator is composition for behaviour: wrap a class you can't modify instead of extending it.
- Go's model proves the point: a production language with no inheritance at all. You almost never miss it.
- In interviews: quote Effective Java Item 18 and say "I reach for composition first and only use inheritance when the IS-A relationship is genuine and LSP-compliant."