All chapters in this track
- 01Chapter 1: Why Object Orientation Exists
- 02Chapter 2: Classes, Objects, and Instances, Precisely Defined
- 03Chapter 3: The Four Pillars I: Encapsulation
- 04Chapter 4: The Four Pillars II: Abstraction
- 05Chapter 5: The Four Pillars III: Inheritance
- 06Chapter 6: The Four Pillars IV: Polymorphism
- 07Chapter 7: Composition vs. Inheritance
- 08Chapter 8: Interfaces and Abstract Classes: When to Use Which
- 09Chapter 9: SOLID, Part 1: Single Responsibility & Open/Closed
- 10Chapter 10: SOLID, Part 2: Liskov Substitution, Interface Segregation, Dependency Inversion
- 11Chapter 11: Basic UML for Communicating Design
- 12Chapter 12: Anti-Patterns: God Objects, Tight Coupling, Deep Hierarchies, the Fragile Base Class, Anemic Domain Models
- 13Chapter 13: OOP Idioms Across TypeScript and Python — and Where OOP Is the Wrong Tool
- 14Chapter 14: When OOP Breaks Down: Real-World Failure Modes
- 15Chapter 15: Design Exercise: Parking Lot System
- 16Chapter 16: Design Exercise: Library Management System
- 17Chapter 17: Design Exercise: Elevator Controller
- 18Chapter 18: OOP in Interviews: What a Strong Answer Looks Like
Chapter 5 of 18
Chapter 5: The Four Pillars III: Inheritance
Why this matters
Inheritance is the pillar with the worst reputation among experienced engineers — not because it's a bad idea, but because it's the most over-applied one. Almost every "OOP horror story" a senior engineer tells involves a deep inheritance hierarchy that became impossible to change. Understanding what inheritance actually buys you, and its real cost, is what lets you use it in the narrow cases where it's genuinely the right tool (Chapter 7 covers the composition alternative in depth).
The core concept
[BEGINNER] Inheritance lets a class (the subclass/derived class) acquire the fields and methods of another class (the superclass/base class), and optionally override or extend them. It models an "is-a" relationship: a Savings Account is a kind of Account.
class Account {
protected balanceCents: number = 0;
constructor(public readonly id: string) {}
deposit(amountCents: number): void {
this.balanceCents += amountCents;
}
getBalanceCents(): number {
return this.balanceCents;
}
}
class SavingsAccount extends Account {
constructor(id: string, private readonly interestRateBps: number) {
super(id);
}
applyMonthlyInterest(): void {
const interest = Math.floor(this.balanceCents * this.interestRateBps / 10000);
this.deposit(interest); // reuses the parent's method
}
}
const savings = new SavingsAccount("acc-1", 50); // 0.5% monthly
savings.deposit(10000);
savings.applyMonthlyInterest();
console.log(savings.getBalanceCents()); // 10050
class Account:
def __init__(self, account_id: str) -> None:
self.id = account_id
self._balance_cents = 0
def deposit(self, amount_cents: int) -> None:
self._balance_cents += amount_cents
def get_balance_cents(self) -> int:
return self._balance_cents
class SavingsAccount(Account):
def __init__(self, account_id: str, interest_rate_bps: int) -> None:
super().__init__(account_id)
self._interest_rate_bps = interest_rate_bps
def apply_monthly_interest(self) -> None:
interest = self._balance_cents * self._interest_rate_bps // 10000
self.deposit(interest)
SavingsAccount gets deposit and getBalanceCents for free, and adds interest-specific behavior on top. This is inheritance doing exactly what it's for: a genuine is-a relationship, with shared behavior that the subclass legitimately wants unchanged.
What inheritance actually costs
[SENIOR INSIGHT] Every subclass is coupled to its superclass's implementation, not just its interface. This is the crucial, under-appreciated cost: a change to the parent class can silently break every subclass, because subclasses often depend on how the parent does things, not just what it promises to do.
class Account {
protected balanceCents: number = 0;
deposit(amountCents: number): void {
this.balanceCents += amountCents;
this.logTransaction("deposit", amountCents); // added later, for auditing
}
protected logTransaction(kind: string, amountCents: number): void {
console.log(`${kind}: ${amountCents}`);
}
}
class SavingsAccount extends Account {
applyMonthlyInterest(interestCents: number): void {
this.balanceCents += interestCents; // bypasses deposit() entirely
// No one remembers to also call logTransaction() here — it was added
// to deposit() long after this subclass was written.
}
}
The subclass author had no way to know that a later change to deposit() (adding logging) would create an inconsistency, because SavingsAccount didn't call deposit() at all — it directly touched the inherited protected field. This is the classic fragile base class problem (covered fully as an anti-pattern in Chapter 12): changes to a base class can break subclasses in ways neither the base class author nor the subclass author can fully predict, because the coupling runs through implementation, not just interface.
The Liskov Substitution litmus test
[BEGINNER] Before creating a subclass, ask: can every caller that uses the base class work correctly if a subclass instance is substituted in, without knowing the substitution happened? If the answer is no, it's not a true is-a relationship for your program's purposes, whatever the real-world hierarchy suggests. (This principle — Liskov Substitution — gets a full chapter, Chapter 10, because violating it is one of the most common inheritance mistakes.)
The textbook example: a Square that extends Rectangle sounds geometrically correct, but if Rectangle has independent setWidth/setHeight methods, a Square must couple them (changing one changes both to stay a square) — which breaks any code that assumed setting a Rectangle's width leaves its height alone. Geometrically, a square is a rectangle. Behaviorally, in this interface, it is not substitutable — so inheritance is the wrong tool here even though the "is-a" language sounds right.
Common mistakes
- Inheriting for code reuse alone, without a real is-a relationship. If
ReportEmailerextendsEmailSenderpurely to reuse asend()method, with no actual is-a relationship, that's reuse smuggled in as inheritance — composition (Chapter 7) is the correct tool. - Deep inheritance chains.
Manager extends Employee extends Person extends LegalEntity— every layer adds coupling; a change anywhere in the chain can ripple through everything below it. Prefer shallow hierarchies (one, at most two levels) in practice. - Overriding a method in a way that violates the caller's expectations of the base type (Liskov substitution again) — e.g., an override that throws where the base class promised it wouldn't, or narrows accepted input types.
- Depending on a superclass's protected fields or private implementation details rather than calling its public methods, as in the
SavingsAccountexample above — this is what makes the base class "fragile" to change.
Checklist: using inheritance well
- Confirm a genuine is-a relationship, verified by substitutability — not just similar-sounding names or a desire to reuse code.
- Prefer calling the parent's public methods over touching its protected fields directly, even from a subclass — it keeps the coupling at the interface level, which is far more stable than coupling to implementation.
- Keep hierarchies shallow. If you're reaching for a third level of inheritance, stop and ask whether composition would model the relationship more precisely.
- When you're inheriting purely to reuse a method with no is-a relationship, use composition instead (a
has-a, via a field, notextends) — Chapter 7 covers the mechanics. - Document (or test) the contract a base class promises, so subclass authors know what they can safely rely on staying stable versus what's an implementation detail that might change.
Discussion / interview questions
-
"What does inheritance model, and how do you know it's the right relationship to use?" Strong answer: Models "is-a"; verified with the substitutability test — can a subclass be used anywhere the base class is expected, without breaking caller assumptions? Not just "these two things sound related."
-
"What is the fragile base class problem, and how have you seen it happen?" Strong answer: A change to a base class's implementation (not just its interface) silently breaks subclasses that depended on the old behavior, often because those subclasses bypassed public methods and touched shared/protected state directly — gives or recognizes a concrete example like the logging scenario above.
-
"Why is
Square extends Rectangleoften cited as a bad inheritance example, even though a square is geometrically a rectangle?" Strong answer: Geometric truth isn't the test — behavioral substitutability is. IfRectangleexposes independent width/height setters, aSquaresubclass can't honor that contract without breaking it, so it fails Liskov substitution despite being "is-a" in plain English. -
"When would you choose inheritance over composition, or vice versa?" Strong answer: Inheritance when there's a genuine, substitutable is-a relationship with behavior the subclass should transparently inherit unchanged; composition when you want to reuse behavior without committing to that substitutability contract, or when the relationship is really "has-a" / "uses-a." (Full comparison in Chapter 7.)
-
"What's the practical cost of a deep inheritance hierarchy, beyond 'it's hard to read'?" Strong answer: Every layer increases the blast radius of a change — a modification anywhere in the chain can silently affect every class below it, and understanding any one class may require reading its entire ancestor chain. Says this concretely, not just "it's confusing."
Chapter summary / cheat sheet
- Inheritance models "is-a" and gives a subclass a superclass's fields/methods, with the option to override or extend.
- Its real cost: subclasses couple to the superclass's implementation, not just its interface — this is the fragile base class problem.
- The litmus test for "is this really inheritance?" is substitutability (Liskov Substitution, Chapter 10): can a subclass replace the base class everywhere, without surprising any caller?
- Keep hierarchies shallow; prefer calling a parent's public methods over touching its protected internals from a subclass.
- If you want to reuse behavior without a true is-a relationship, that's a signal for composition (Chapter 7), not inheritance.