Early Access: 87 spots left.

Claim

Course content

No content available

The Liskov Substitution Principle says that a subtype must be usable wherever its supertype is expected. If your code holds a reference of the base type and calls a method on it, the call should behave correctly regardless of which specific subtype the reference is actually pointing at.

The principle is named after Barbara Liskov, who formulated it in terms of contracts: a subtype must satisfy every promise the supertype makes. If the supertype says "this method returns a positive number", the subtype cannot return a negative one. If the supertype says "this method does not throw", the subtype cannot throw. The supertype is a published contract, and the subtype is bound by it.

In this module's running example, the checkout backend of an online store (the full tour), the slice for this principle is the customer balance accounts: a standard account whose balance can be withdrawn back to the customer's bank, and a fixed-deposit account for promotional balances that stay locked until maturity. The page stands on its own if you have not read the tour.

Most LSP violations come from designs that use inheritance because two things sound related, even when one cannot honour the other's contract. "A square is a rectangle." "A penguin is a bird." "A fixed deposit is an account." These are all natural English statements, and all three lead to broken hierarchies.

A square cannot honour a rectangle's contract that width and height vary independently. A penguin cannot honour a bird's contract to fly. A fixed deposit cannot honour an account's contract to allow withdrawal. In each case, the answer is the same: stop modelling the relationship as inheritance. Find a smaller contract that both types can honour, and put the variable capability on a separate interface.

SmellWhat it looks likeWhy it's a problem
A subclass throws on an inherited methodvoid withdraw(...) { throw new UnsupportedOperationException(...); }The base type promises a capability the subtype cannot deliver
A subclass narrows the contract of an inherited methodReturns a smaller range, accepts fewer arguments, throws on inputs the base acceptsCallers written against the base now break when given the subtype
Type checks immediately before a method callif (account instanceof FixedDeposit) skipWithdraw(); else account.withdraw(amount);The caller is working around an LSP violation that should have been fixed in the type system
A boolean capability flag on the base classif (account.supportsWithdrawal()) account.withdraw(amount);The capability is being checked at runtime instead of expressed at compile time
An empty or no-op implementation of an inherited methodvoid withdraw(...) { /* do nothing */ }The subclass silently discards a request the base type told the caller would succeed

Any function that accepts an Account reference and calls withdraw works for SavingsAccount but throws for FixedDepositAccount. The caller has no way to know in advance, because the base type's signature does not advertise the limitation. The bug only shows up at runtime, in production, when somebody passes a fixed deposit to code that was tested with a savings account.

Defensive callers end up adding type checks like if (account instanceof FixedDepositAccount) skip(). Each such check is a confession that the type system has been lied to. The right answer is to fix the lie at the type level, not to paper over it at every call site.

What this costs in production

A nightly batch job iterated over every Account and called withdraw to settle fees. It had been tested against savings accounts and worked, but the first time a customer opened a FixedDepositAccount the job hit an UnsupportedOperationException and aborted halfway. Half the customers were charged, half were not, and the base-class signature gave no hint that a subtype could refuse the call.

The base class now promises only what every account can honour: a balance, and the ability to deposit. Both subtypes deliver on that promise without any caveats.

The withdraw capability lives on a separate interface, Withdrawable. Code that needs to withdraw asks for a Withdrawable, not an Account. The compiler refuses any attempt to call withdraw on a FixedDepositAccount, because the method is not on its API. The runtime exception that used to surface the LSP violation has been replaced by a compile error, which is exactly the kind of safety the type system is supposed to give you.

Note that the refactor is more about contract size than about inheritance vs. interfaces. The deeper fix is that the base type's contract was too wide. Once the contract is narrowed to what every subtype can honour, the hierarchy becomes safe to substitute, regardless of the syntactic vehicle (interface, abstract class, sealed type) used to represent it.

extendsimplementsextendsAccountWithdrawableSavingsAccountFixedDeposit
Withdraw moves to a separate Withdrawable interface. SavingsAccount is both an Account and Withdrawable; FixedDeposit is only an Account, so code that calls withdraw can never be handed one.

The fixed-deposit example shows one specific way to break a supertype's contract, a new failure mode, but the inherited contract has four distinct clauses. Interviews regularly probe whether you know the other three.

RuleWhat it meansA violation
Preconditions must not be strengthenedThe subtype cannot demand more from callers than the base didBase accepts any positive amount; the subtype rejects withdrawals under 500
Postconditions must not be weakenedThe subtype cannot deliver less than the base promisedBase guarantees the balance never goes negative; an overdraft subtype lets it
Invariants must be preservedWhatever the base keeps true at all times stays trueRectangle keeps width and height independent; Square silently changes both
No new failure modesError handling written against the base must still sufficeFixedDepositAccount throwing on an inherited withdraw, the example above

The invariant row is the famous one. A square is a rectangle in every geometry lesson, and modelling it that way breaks substitution immediately.

The caller relied on a property every Rectangle honours: setting the height does not change the width. Square cannot keep that promise, so it is not a behavioural subtype of Rectangle, whatever the geometry says. That is the general test for inheritance. Never whether the sentence "X is a Y" sounds right in English, always whether X keeps all four of Y's promises.

Six months later the product team adds a recurring deposit, a second account type whose balance is locked. In the original hierarchy this is precisely how the settlement disaster happened: a new subtype quietly overrides withdraw to throw, and every polymorphic loop over accounts becomes a landmine again.

In the refactored hierarchy, the new type simply does not implement Withdrawable.

What this saves in production

The war-story bug is not just fixed, it is unwritable: the settlement job iterates Withdrawable accounts, and the compiler guarantees a locked account can never reach it. New account types are purely additive releases, and the nightly job has not needed a retest since the contract was narrowed.

LSP is not a rule against ever throwing or ever overriding. A subclass is allowed to add behaviour, accept a wider range of inputs, or return a more specific result. What it cannot do is contradict the supertype's promise.

A related trap is splitting hierarchies prematurely. If you have one base type and one subtype today, and the subtype is a faithful refinement, you do not need to extract an interface just because you might add another subtype later. Wait until you actually have a subtype that cannot honour the contract. The LSP violation is the signal, and the fix is local: pull the offending capability onto its own interface. Do not pre-emptively shred the hierarchy into a dozen interfaces just in case.

The companion exercise gives you an Account hierarchy where FixedDepositAccount overrides withdraw to throw. Refactor it so any Account reference is safe to use polymorphically. The validator's reflection check confirms that FixedDepositAccount no longer exposes a withdraw method through any inheritance path, and that SavingsAccount implements the new Withdrawable interface.

Back to Low Level Design