How would you decide between composition and inheritance when designing a class, and when is inheritance still the right choice?
answer
- Default composition; inheritance = justified exception
- 3 tests: true IS-A (LSP)? own+design base? reuse vs subtype?
- Fragile base class + leaked API = inheritance's costs
- Inheritance OK: template method, sealed variants, real subtype
- Design for inheritance or prohibit it (final); prefer interfaces
basics
~20 sUse inheritance only when there's a true, lasting IS-A relationship and the subtype can stand in for the supertype everywhere. Otherwise prefer composition: hold the other object as a field and forward to it. Inheritance is the strongest coupling, so make it a deliberate, narrow choice.
solid answer
~60 sThe default is composition; inheritance is the exception that needs justification. I ask three things. First, is there a genuine, permanent IS-A relationship that satisfies Liskov — can the subtype be substituted for the supertype in every context without surprising callers? If not, it's composition. Second, do I control and version the superclass together with the subclass, in the same package/module, and is it designed for extension (documented self-use, protected hooks, stable contract)? Inheritance across a package or library boundary is dangerous because the base can change underneath you (fragile base class). Third, do I only want to *reuse code*, or do I want *to be a subtype*? Reuse alone is satisfied better by composition + delegation, which avoids leaking the parent's API and keeps me coupled only to a public interface. Inheritance still wins for true subtypes, for framework template-method designs, and for sealed hierarchies modeling a closed set of variants — places where substitutability is real and the hierarchy is intentional. Heuristics: program to interfaces, keep hierarchies shallow, and design-for-extension-or-prohibit-it (final).
code
java · 21 lines// Inheritance done right: a closed, intentional hierarchy
sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double s) implements Shape {}
double area(Shape shape) {
return switch (shape) { // exhaustive, no default needed
case Circle c -> Math.PI * c.r() * c.r();
case Square s -> s.s() * s.s();
};
}
// Composition done right: reuse behavior without subtyping
final class RateLimitedClient {
private final HttpClient delegate; // HAS-A, program to the abstraction
private final RateLimiter limiter;
RateLimitedClient(HttpClient delegate, RateLimiter limiter) {
this.delegate = delegate; this.limiter = limiter;
}
Response send(Request r) { limiter.acquire(); return delegate.send(r); }
}go deeper
Knows the slogan 'favor composition over inheritance' and that inheritance is extends/IS-A while composition is a held field; can pick composition for a simple 'uses-a' case.
Justifies the default with concrete reasons — leaked API, tight coupling — and uses delegation to reuse behavior; recognizes a false IS-A like Stack extends Vector.
Applies LSP and the fragile-base-class problem to decide, designs for extension or marks classes final, and knows the legitimate inheritance cases (template method/skeletal classes, interface implementation).
Reasons about coupling, evolution cost, and boundaries (own vs third-party, package/module) when shaping hierarchies; chooses sealed types for closed variant sets, sets codebase conventions (interfaces-first, shallow hierarchies, final-by-default), and can defend why a given hierarchy should or shouldn't exist given how the system will change.
## The decision: composition vs inheritance Both let you build a new class out of existing ones, but they couple very differently. **Inheritance** (`extends`) is the **tightest** coupling Java offers: the subclass inherits state and behavior, depends on the superclass's *implementation*, and exposes the superclass's entire public API. **Composition** (hold a field + delegate) couples you only to another type's **public interface**. The widely cited guidance — *Effective Java* Item 18, *Design Patterns* — is **'favor composition over inheritance'**: make composition the default and inheritance a justified exception. ### Three questions to decide **1. Is there a true, permanent IS-A relationship?** Inheritance asserts the subtype *is a kind of* the supertype, **forever**, and must obey the **Liskov Substitution Principle (LSP)**: any code written against the supertype must work unchanged when handed the subtype. The classic violation is `Stack extends Vector` (a stack is not really a list — Vector's `add(index, e)` lets callers break stack semantics) and `Properties extends Hashtable` (lets callers bypass the String-only invariant). If you can't honestly substitute the subtype everywhere, the IS-A is false — use composition. **2. Do you own and co-version the base, and is it designed for extension?** Inheritance only across a boundary you control is safe. The **fragile base class problem**: a subclass silently depends on *how* the superclass is implemented (e.g. that `HashSet.addAll` calls `add`), so a future change to the base — even a behavior-preserving one — can break subclasses you don't see. Inheriting from a class in another package/library is especially risky because its authors can change internals between releases. *Effective Java* Item 19: **'design and document for inheritance or else prohibit it'** — a class meant to be subclassed must document its **self-use** (which methods call which overridable methods), expose **protected hooks**, and never call overridable methods from its constructor; everything else should be **`final`**. **3. Do you want code reuse, or to be a subtype?** If you only want to *reuse behavior*, composition + **delegation** does it without the two problems above and without **leaking the parent's API** (inheritance forces every public superclass method onto your class whether it fits or not). If you genuinely need polymorphic substitutability — callers holding the supertype must dispatch to your behavior — that's what inheritance (or interface implementation) is *for*. ### Costs of getting inheritance wrong - **Leaked, nonsensical API:** subclass exposes parent methods that don't fit (Stack/Vector). - **Fragile base class:** base changes break subclasses; the override-and-self-call traps (double-counted `addAll`). - **Rigid hierarchy / class explosion:** combining N orthogonal variations by inheritance needs N×M subclasses; composition combines them with M+N pieces (this is precisely the **Bridge**/**Strategy**/**Decorator** insight). - **Constructor-overridable-method pitfall:** a base constructor calling an overridable method runs the subclass override before the subclass is initialized. ### When inheritance is still the right call - **True subtype with stable substitutability** that you control (a genuine specialization). - **Template Method / skeletal implementations:** `AbstractList`, `AbstractMap` define an algorithm skeleton with `abstract`/hook methods for subclasses — inheritance is the intended extension mechanism, and the base is *designed* for it. - **Sealed hierarchies** modeling a **closed set of variants** (`sealed interface Shape permits Circle, Square`) consumed by pattern-matching `switch` — here inheritance expresses an exhaustive algebraic type, and the `permits` clause keeps it controlled. - **Interface implementation** (`implements`) — this is 'inheritance of type,' the safe kind: multiple allowed, no implementation coupling; *prefer it* and pair with default methods or composition for shared code. ### Practical heuristics - **Program to interfaces, not implementations** — depend on the abstraction; compose concrete pieces behind it. - **Keep hierarchies shallow** — deep trees compound fragility. - **Default to `final`** for classes not designed for extension; open them deliberately. - **Prefer interface + composition** to share behavior across unrelated types instead of forcing a common base. - **Use the relationship test:** can you truly say *is-a* (substitutable, permanent)? Inheritance. Otherwise *has-a/uses-a* → composition. ### One-line takeaway Composition is the safe default because it couples to a public interface and can evolve; inheritance is a powerful but tight tool reserved for genuine, controlled, substitutable IS-A relationships and for hierarchies explicitly designed (template methods, sealed types) to be extended.
- Why is inheriting from a class in a different library especially risky?You don't co-version the base, so its maintainers can change internal behavior between releases that your subclass silently depended on (the fragile base class problem). A behavior-preserving refactor on their side can still break your overrides or self-call assumptions, and you have no control over the timing. Composition against their public interface, or a documented extension point, is safer.
- Give an example where the standard library itself misused inheritance.java.util.Stack extends Vector, so a Stack inherits Vector methods like add(index, element) and remove(index) that let callers violate LIFO semantics — a false IS-A. Properties extends Hashtable similarly lets callers bypass its String-key/String-value invariant via Hashtable's put. Both would have been better modeled by composing the underlying structure behind a narrow interface.
- How do sealed classes change the inheritance calculus?Sealed hierarchies let you model a closed, exhaustive set of variants with a permits clause, so the hierarchy is controlled (no surprise external subclasses) and pattern-matching switches can be checked for exhaustiveness without a default. That makes inheritance a deliberate, safe modeling tool for algebraic-data-type-style designs, rather than an open-ended reuse mechanism.
Inheritance is welding two parts into one casting — strong but you can never separate or swap them. Composition is bolting parts together — slightly more hardware, but you can replace any piece without re-melting the whole thing.
saying these in an interview costs you the question
- Reaching for inheritance to reuse code without a real IS-A relationship.
- Subclassing a class from another library/package that wasn't designed and documented for extension.
- Treating 'favor composition over inheritance' as 'never use inheritance' — template methods and sealed hierarchies are valid inheritance.
- Ignoring LSP: creating a subtype that can't safely substitute for its supertype (Stack/Vector).
- Calling overridable methods from a base-class constructor.