LESSON 2.3
Liskov substitution
A subclass must be usable wherever its parent is used. If it strengthens preconditions or weakens postconditions, it is not a true substitute.
6 min read
A BankAccount base class has a withdraw(amount) method. A SavingsAccount subclass overrides it to throw when the balance would drop below a $100 minimum. Code that accepts a BankAccount and calls withdraw suddenly crashes with an unexpected exception — even though the runtime type is technically a bank account.
The problem
class Account {
#balance;
constructor(balance) { this.#balance = balance; }
get balance() { return this.#balance; }
withdraw(amount) {
if (amount > this.#balance) throw new Error("Insufficient funds");
this.#balance -= amount;
}
}
class SavingsAccount extends Account {
withdraw(amount) {
// Adds a new precondition: balance must stay above 100.
if (this.balance - amount < 100) throw new Error("Below minimum");
super.withdraw(amount);
}
}
function processWithdrawal(account, amount) {
account.withdraw(amount); // Crashes if account is a SavingsAccount and balance - amount < 100.
}processWithdrawal accepts an Account, so every caller assumes they can safely withdraw if amount <= balance. SavingsAccount silently breaks that assumption by adding a new precondition.
The rule
Liskov substitution principle: if S is a subtype of T, then every object of type T in a program may be replaced by an object of type S without altering any of the correct properties of that program.
The two most common violations:
- Strengthened preconditions — the subclass requires more than the parent (new guards, restricted inputs).
- Weakened postconditions — the subclass produces less than the parent (throwing instead of returning, returning a narrower set of values).
In code
Make the minimum-balance rule explicit in SavingsAccount.canWithdraw instead of strengthening the inherited postcondition:
Spotting violations early
LSP violations usually appear as instanceof checks inside callers:
if (account instanceof SavingsAccount) { /* handle minimum */ }
account.withdraw(amount);Every instanceof guard in a polymorphic call site is a symptom. Move the variation into the subclass, or use composition instead of inheritance.
Where you have already seen it
Java's ArrayList and LinkedList both implement List. Code that accepts a List works with either. Java's Stack extends Vector and does not satisfy LSP — Vector.add() inserts at any position, but a stack should only push to the top, making Stack a famous example of inheritance misuse in the standard library.
Try it
Finite State Machine Engine LLD
State transitions must satisfy LSP: any State subclass must accept the same set of events as the base State.