Loading content...
"Favor Composition Over Inheritance" is a heuristic, not an absolute law. The word "favor" is deliberate—it implies that inheritance has legitimate uses and that blindly applying composition everywhere is as harmful as reflexively using inheritance.
Mature engineering judgment requires knowing not just when to apply a principle, but when to break from it. This page examines scenarios where inheritance is the better choice, helping you develop the nuanced decision-making that distinguishes expert practitioners from rule followers.
The goal isn't to replace inheritance dogma with composition dogma. It's to develop informed judgment. Engineers who reflexively reject inheritance are as ineffective as those who reflexively use it. Both are following rules without understanding.
This page covers: genuine IS-A relationships with behavioral substitutability, framework extension requirements, template method scenarios, immutable type hierarchies, reusing complex behavior efficiently, language idioms that favor inheritance, and a decision framework for choosing between composition and inheritance.
Inheritance is appropriate when there's a genuine IS-A relationship that satisfies the Liskov Substitution Principle (LSP): subclasses must be fully substitutable for their parent in all contexts without altering correctness.
The LSP Test:
If code expects a Parent type, it should work correctly with any Child type without knowing it's a child. The child must:
123456789101112131415161718192021222324252627282930313233343536373839404142
// LEGITIMATE INHERITANCE: Java Collections Framework // List is a genuine IS-A relationship with Collectionpublic interface List<E> extends Collection<E> { // List IS-A Collection with additional ordering guarantees // Any code expecting Collection works perfectly with List} // ArrayList and LinkedList are genuinely IS-A Listspublic class ArrayList<E> extends AbstractList<E> implements List<E> { // ArrayList IS-A List - it honurs ALL List contracts // Performance characteristics differ, but behavior contracts are identical} public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E> { // LinkedList IS-A List AND IS-A Deque // Fully substitutable for both} // This code works correctly with ANY List implementationpublic void processItems(List<String> items) { for (String item : items) { // Works identically whether ArrayList, LinkedList, or any List process(item); } // Random access - works on any List (though performance varies) String first = items.get(0); // Size query - contracts honored by all implementations if (items.size() > 10) { items.subList(10, items.size()).clear(); }} // WHY THIS WORKS AS INHERITANCE:// 1. True IS-A: ArrayList genuinely IS-A List, not just "sort of like" a List// 2. Full substitutability: Any code expecting List works with ArrayList// 3. Contract preservation: ArrayList honors all List method contracts// 4. Behavioral consistency: Iterator, equals, hashCode all work correctly// 5. The hierarchy is STABLE and CLOSED (you don't add new core List types often)Contrast: The Rectangle-Square Problem
Not all mathematical IS-A relationships translate to behavioral IS-A:
setWidth() and setHeight() to be independent operations, Square breaks this1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
// LSP VIOLATION: Square extends Rectangle public class Rectangle { protected int width; protected int height; public void setWidth(int width) { this.width = width; } public void setHeight(int height) { this.height = height; } public int getArea() { return width * height; }} public class Square extends Rectangle { @Override public void setWidth(int width) { this.width = width; this.height = width; // PROBLEM: Side effect! } @Override public void setHeight(int height) { this.width = height; this.height = height; // PROBLEM: Side effect! }} // This code FAILS for Square even though it works for Rectanglepublic void testRectangle(Rectangle r) { r.setWidth(5); r.setHeight(4); assert r.getArea() == 20; // FAILS for Square! Area is 16 // Width was 5, then setHeight(4) changed width to 4} // SOLUTION: Don't use inheritance here// Use composition or separate type hierarchies public interface Shape { int getArea();} public final class Rectangle implements Shape { private final int width; private final int height; public Rectangle(int width, int height) { this.width = width; this.height = height; } @Override public int getArea() { return width * height; }} public final class Square implements Shape { private final int side; public Square(int side) { this.side = side; } @Override public int getArea() { return side * side; }} // No inheritance, no substitutability problemsBefore using inheritance, ask: "Can I put the child into ANY code expecting the parent and have it work correctly?" If there are scenarios where the child would break parent expectations, use composition instead.
Many frameworks are designed for inheritance. They provide base classes with template methods, lifecycle hooks, and protected utilities that expect you to extend them.
When framework extension requires inheritance, use inheritance.
Fighting against a framework's design philosophy creates more problems than it solves. The framework authors made inheritance-friendly designs deliberately—often because they need to control the template of execution while allowing you to customize specific steps.
123456789101112131415161718192021222324252627282930313233343536373839404142
// FRAMEWORK EXTENSION: Android Activity Lifecycle // Android REQUIRES extending Activity/AppCompatActivity// The framework controls the lifecycle; you customize specific hookspublic class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // MUST call super setContentView(R.layout.activity_main); // Your initialization code initializeViews(); loadData(); } @Override protected void onResume() { super.onResume(); // Resume-specific logic startLocationUpdates(); } @Override protected void onPause() { super.onPause(); stopLocationUpdates(); } @Override protected void onDestroy() { cleanup(); super.onDestroy(); }} // WHY INHERITANCE IS CORRECT HERE:// 1. Framework REQUIRES it - no composition alternative exists// 2. Framework controls the TEMPLATE (lifecycle sequence)// 3. You customize SPECIFIC STEPS within that template// 4. The relationship is genuine: MainActivity IS-AN Activity// 5. All Activity contracts are honoredNotice that modern framework design increasingly favors composition (React Hooks, Spring Dependency Injection, Kotlin Android Extensions). This validates the principle while acknowledging that legacy frameworks and some use cases genuinely require inheritance.
The Template Method Pattern deliberately uses inheritance to define the skeleton of an algorithm while allowing subclasses to customize specific steps. This is inheritance by design, not by default.
When Template Method is appropriate:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
// TEMPLATE METHOD: Fixed Algorithm, Customizable Steps public abstract class DataExporter { // The TEMPLATE: defines the algorithm structure // This method is FINAL - subclasses cannot change the sequence public final void export(Dataset data, OutputStream output) { validateData(data); // Step 1: Always validate Dataset prepared = prepareData(data); // Step 2: Prepare (customizable) byte[] formatted = formatData(prepared); // Step 3: Format (customizable) writeData(formatted, output); // Step 4: Write (customizable) logExport(data); // Step 5: Always log } // Fixed steps (private or final) private void validateData(Dataset data) { if (data == null || data.isEmpty()) { throw new IllegalArgumentException("Cannot export empty dataset"); } } private void logExport(Dataset data) { logger.info("Exported {} records", data.size()); } // Customizable steps (protected abstract or with default) protected Dataset prepareData(Dataset data) { return data; // Default: no preparation } protected abstract byte[] formatData(Dataset data); protected void writeData(byte[] data, OutputStream output) { output.write(data); // Default: direct write }} // Subclasses customize ONLY the relevant stepspublic class CsvExporter extends DataExporter { @Override protected byte[] formatData(Dataset data) { StringBuilder csv = new StringBuilder(); csv.append(String.join(",", data.getHeaders())).append("\n"); for (Row row : data.getRows()) { csv.append(String.join(",", row.getValues())).append("\n"); } return csv.toString().getBytes(StandardCharsets.UTF_8); }} public class JsonExporter extends DataExporter { @Override protected byte[] formatData(Dataset data) { return objectMapper.writeValueAsBytes(data); }} public class EncryptedExporter extends DataExporter { private final Cipher cipher; @Override protected byte[] formatData(Dataset data) { return new JsonExporter().formatData(data); // Reuse JSON formatting } @Override protected void writeData(byte[] data, OutputStream output) { byte[] encrypted = cipher.doFinal(data); super.writeData(encrypted, output); // Encrypt before write }} // WHY TEMPLATE METHOD USES INHERITANCE CORRECTLY:// 1. Algorithm STRUCTURE is fixed and controlled by parent// 2. Subclasses can ONLY customize designated extension points// 3. Parent ensures validation and logging ALWAYS happen// 4. The 'final' template method prevents breaking the algorithm// 5. Clear, limited set of customization pointsTemplate Method controls the algorithm and allows step customization (inheritance). Strategy allows complete algorithm replacement (composition). Use Template Method when you MUST control the overall flow. Use Strategy when you want full flexibility in behavior.
Inheritance works well for immutable value types where:
Immutability removes many of the problems that make inheritance dangerous—there's no fragile base class problem when nothing mutates.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
// IMMUTABLE TYPE HIERARCHY: Expression Trees // Sealed hierarchy for mathematical expressionspublic sealed abstract class Expression permits Constant, Variable, BinaryOp, UnaryOp { // All subclasses must implement evaluation public abstract double evaluate(Map<String, Double> variables); // Immutable operations return new expressions public Expression plus(Expression other) { return new BinaryOp(Operator.ADD, this, other); } public Expression times(Expression other) { return new BinaryOp(Operator.MULTIPLY, this, other); }} public final class Constant extends Expression { private final double value; public Constant(double value) { this.value = value; } @Override public double evaluate(Map<String, Double> variables) { return value; }} public final class Variable extends Expression { private final String name; public Variable(String name) { this.name = name; } @Override public double evaluate(Map<String, Double> variables) { return variables.get(name); }} public final class BinaryOp extends Expression { private final Operator operator; private final Expression left; private final Expression right; public BinaryOp(Operator operator, Expression left, Expression right) { this.operator = operator; this.left = left; this.right = right; } @Override public double evaluate(Map<String, Double> variables) { return operator.apply( left.evaluate(variables), right.evaluate(variables) ); }} // Usage: Build and evaluate expressionsExpression expr = new Variable("x") .times(new Variable("x")) .plus(new Constant(2).times(new Variable("x"))) .plus(new Constant(1));// Represents: x² + 2x + 1 double result = expr.evaluate(Map.of("x", 3.0)); // = 16.0 // WHY INHERITANCE WORKS HERE:// 1. IMMUTABLE - no state changes, no fragile base class problem// 2. SEALED - closed hierarchy, all subtypes are known// 3. GENUINE IS-A - Constant genuinely IS-AN Expression// 4. LSP SATISFIED - all subtypes are fully substitutable// 5. Pattern matching works well with sealed type hierarchiesJava 17+ sealed classes, Kotlin sealed classes, and Scala sealed traits provide language support for closed hierarchies. Pattern matching and exhaustiveness checking make inheritance-based hierarchies safe and powerful for these use cases.
Sometimes inheritance provides significant implementation reuse that would be wasteful to replicate via composition. When you have:
Inheritance can be more efficient than wrapper/delegate approaches that replicate method forwarding.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
// EFFICIENT REUSE: AbstractMap in Java Collections // AbstractMap provides 90% of Map implementation// Subclasses only need to implement entrySet()public class SimpleMap<K, V> extends AbstractMap<K, V> { private final List<Entry<K, V>> entries = new ArrayList<>(); @Override public Set<Entry<K, V>> entrySet() { return new LinkedHashSet<>(entries); } @Override public V put(K key, V value) { for (Entry<K, V> entry : entries) { if (Objects.equals(entry.getKey(), key)) { V old = entry.getValue(); ((SimpleEntry<K, V>) entry).setValue(value); return old; } } entries.add(new SimpleEntry<>(key, value)); return null; }} // AbstractMap provides these for free:// - get(Object key)// - containsKey(Object key)// - containsValue(Object value)// - remove(Object key)// - putAll(Map<? extends K, ? extends V> map)// - clear()// - keySet()// - values()// - equals(Object o)// - hashCode()// - toString() // If we used composition/delegation, we'd need:public class ComposedMap<K, V> implements Map<K, V> { private final List<Entry<K, V>> entries = new ArrayList<>(); @Override public V get(Object key) { // Must implement manually for (Entry<K, V> entry : entries) { if (Objects.equals(entry.getKey(), key)) { return entry.getValue(); } } return null; } @Override public boolean containsKey(Object key) { // Must implement manually return get(key) != null; } @Override public Set<K> keySet() { // Must implement manually Set<K> keys = new HashSet<>(); for (Entry<K, V> entry : entries) { keys.add(entry.getKey()); } return keys; } // ... 12+ more methods to implement manually} // The inheritance approach is CLEANER here because:// 1. AbstractMap is STABLE and well-designed// 2. The reuse is SUBSTANTIAL (12+ methods)// 3. The IS-A relationship is GENUINE (SimpleMap IS-A Map)// 4. Customization is LIMITED (just entrySet() and optionally put())Be careful: "we want the parent's functionality" is often a sign of wrong inheritance. The question isn't "do we want this behavior?" but "are we genuinely a subtype that should have this behavior?" Dogs don't inherit from Cars just because both have an 'engine' they want to reuse.
Different programming languages and their ecosystems have different idioms. Following established idioms improves code readability and reduces friction with team members and the broader community.
Examples where inheritance is idiomatic:
| Context | Idiomatic Approach | Rationale |
|---|---|---|
| Python exceptions | class CustomError(Exception) | Standard exception hierarchy pattern |
| Java Swing/JavaFX | class CustomPanel extends JPanel | Framework expectation |
| C++ RAII classes | class Derived : public Base | Destructor chaining |
| Django models | class Product(models.Model) | ORM pattern |
| React (legacy) | class App extends React.Component | Pre-hooks standard |
| Go | Composition only (no inheritance) | Language design choice |
| Rust | Traits + composition | No class inheritance exists |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
# IDIOMATIC: Custom Exception Hierarchy # Base application exceptionclass ApplicationError(Exception): """Base exception for all application errors.""" def __init__(self, message: str, code: str = None): super().__init__(message) self.code = code # Domain-specific exceptionsclass ValidationError(ApplicationError): """Raised when input validation fails.""" def __init__(self, field: str, message: str): super().__init__(f"Validation failed for {field}: {message}") self.field = field class NotFoundError(ApplicationError): """Raised when a requested resource is not found.""" def __init__(self, resource_type: str, resource_id: str): super().__init__( f"{resource_type} with id '{resource_id}' not found", code="NOT_FOUND" ) self.resource_type = resource_type self.resource_id = resource_id class AuthorizationError(ApplicationError): """Raised when user lacks permission.""" def __init__(self, action: str, resource: str): super().__init__( f"Not authorized to {action} on {resource}", code="FORBIDDEN" ) # Usage: Catch at appropriate levelstry: user = user_service.get_by_id(user_id)except NotFoundError: return {"error": "User not found"}, 404except ApplicationError as e: return {"error": str(e), "code": e.code}, 400 # This inheritance is IDIOMATIC because:# 1. Python's exception handling is designed for hierarchies# 2. 'except ApplicationError' catches all app errors# 3. Standard practice across Python ecosystem# 4. The hierarchy represents genuine type relationshipsIf the entire ecosystem uses inheritance for a particular pattern (like exceptions), using composition would confuse readers and fight against tooling. Follow idioms unless there's a compelling reason not to.
Use this decision framework when choosing between inheritance and composition:
The Quick Questions:
"Is this a true IS-A?" — Does the child genuinely represent a subtype, or are you just reusing methods?
"Would I need to override methods to 'turn off' behavior?" — If yes, it's not a true IS-A.
"Could the relationship change?" — If the answer is maybe, composition is safer.
"Am I fighting the framework?" — If the framework expects inheritance, use it.
"What would I test?" — If you'd need to mock the parent, composition would be easier.
The principle says FAVOR composition. In practice: start with composition as your default assumption. Only choose inheritance when you have a clear justification that matches one of the scenarios in this page. The burden of proof should be on inheritance.
The mark of an experienced engineer isn't knowing rules—it's knowing when rules apply and when they don't. "Favor Composition Over Inheritance" is powerful guidance, but applying it dogmatically creates its own problems.
| Scenario | Key Indicator |
|---|---|
| True IS-A with LSP | Child fully substitutable for parent in all contexts |
| Framework extension | Framework provides base classes for extension |
| Template Method | Fixed algorithm, customizable steps |
| Immutable hierarchies | Closed type hierarchy, no state mutation |
| Substantial reuse | Stable parent with significant shared implementation |
| Language idioms | Established pattern in the ecosystem (e.g., exceptions) |
You've completed the 'Favor Composition Over Inheritance' module. You understand the principle's origins, why composition is often preferred, the flexibility benefits it provides, AND when inheritance is the right choice. This balanced understanding—knowing both when to apply and when to break from a principle—is the hallmark of senior engineering judgment.