The idea
What it is
The one sentence to remember
If code works with a base type, it must keep working when given any subtype — no surprises.
The Liskov Substitution Principle (the L in SOLID) is about one promise: anywhere your code expects a base type, you should be able to drop in any subtype and the code keeps doing the right thing. The substitute behaves like what it claims to be. If swapping in a subclass quietly changes the result, throws an error the caller didn't expect, or does less than the base promised, you've broken LSP.
Think of a vending machine that takes any coin marked “1 dollar.” If someone slips in a fake that's the right size and weight but worth nothing, the machine still accepts it and your snack vanishes into a loss. The fake looked like a dollar (same shape) but didn't behave like one (same value). LSP says a subtype must be a real dollar — same shape and same behaviour — so the machine (your code) never gets a nasty surprise.
Barbara Liskov framed it in 1987 and it later became the third letter of SOLID. The key shift in thinking: inheritance isn't just “this class has the same fields and methods.” It's a contract. The subtype inherits the base's promises, and it must honour every one of them.
Mechanics
How it works
Two classic violation stories
The fastest way to feel LSP is to see it break. There are two stories every engineer should know by heart.
Story 1 — the Rectangle and the Square. In maths, a square is a rectangle, so it's tempting to write class Square extends Rectangle. A rectangle has independent setWidth and setHeight. But a square must keep all sides equal, so its setWidth also changes the height (and vice versa). Now picture a caller that was written for a plain rectangle:
area == 20 holds. The Square ties them together: setHeight(4) quietly clobbers the width you just set, so the box is 4 × 4 and area == 16. The caller did nothing wrong — the subtype broke a promise the base made. That's an LSP violation.Story 2 — the Bird that can't fly. Put a fly() method on a base Bird. Most birds are fine. Then along comes Penguin extends Bird — penguins don't fly. To compile, you override fly() to throw new UnsupportedOperationException(). Now any code that loops over birds and calls fly() blows up the moment it meets a penguin. The base promised “all birds fly”; the subtype can't keep that promise, so it cheats by throwing.
The tell-tale smell: a 'throw new NotSupportedException' override
If a subclass overrides a method only to throw ("not supported here") or to do nothing at all, that's a degenerate override — a loud signal it doesn't truly satisfy the base's contract. The subtype removed behaviour the base promised. That's almost always an LSP violation, and a hint your inheritance tree is wrong.
The rule-of-thumb tests
You don't need formal logic to apply LSP. Run a candidate override through these four checks — a real subtype passes all of them:
- Don't strengthen preconditions. A subtype must not demand more of the caller than the base did. If the base accepts any integer but the subtype rejects negatives, callers written for the base will suddenly fail.
- Don't weaken postconditions. A subtype must deliver at least what the base promised to return or guarantee. If the base says “returns a sorted list,” the subtype can't hand back an unsorted one.
- Don't throw new, unexpected exceptions. A subtype shouldn't throw error types the caller never agreed to handle — the
Penguin.fly()trap. (Throwing the same kinds the base already documents is fine.) - Don't remove behaviour the base promised. No degenerate overrides — no
throw new NotSupportedException, no silently doing nothing where the base did something real.
is-a vs behaves-like-a
A square is a rectangle in geometry, and a penguin is a bird in biology — the shapes match. But neither behaves like the base your code relies on. LSP is the discipline of asking the harder question: not “is X a kind of Y?” but “can X stand in for Y everywhere Y is used, and keep every promise Y made?” Inheritance must preserve the contract, not just the field-and-method shape.
Why this is really about the caller
LSP protects the code that uses the base type. That code was written against the base's promises and shouldn't need a single if (x instanceof Square) special-case to stay correct. When you find yourself adding type checks to handle one particular subclass, the subclass has already broken substitutability — and you're now paying for it in branches scattered across the codebase.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
A substitution test bench. The caller code is fixed and you only change which object you pass in. In the Rectangle vs Square scenario the caller runs setWidth(5); setHeight(4) and expects area == 20. Pass a Rectangle and it passes (green, area 20). Pass a Square and watch its width-set silently also change its height — the expectation breaks (red, area 16). In the Bird.fly() scenario a Sparrow flies fine but a Penguin throws. The fixed explain panel names the exact rule that was broken in plain words, and a Fixed design view shows the corrected model where every substitute passes. Only one note shows at a time — nothing scrolls away or grows the page.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
Watch the Square break the promise
Open the prototype on the Rectangle vs Square scenario. The caller is fixed: setWidth(5); setHeight(4) expecting area == 20. First click Pass a Rectangle and confirm it lands green at 20. Now click Pass a Square and watch the animation — see setHeight(4) silently drag the width down with it, so the box collapses to 4 × 4 and the result turns red at 16. Read the explain panel: which exact rule did the Square violate?
Make the Penguin throw
Switch to the Bird.fly() scenario. Pass a Sparrow first — it flies, green check. Then pass a Penguin and watch the caller's fly() call blow up with a thrown exception (red). Notice that the caller code never changed; only the object you substituted did. The explain panel should name this as throwing a new, unexpected exception — a degenerate override.
See the fixed design pass for everyone
Open the Fixed design view. Here Square is no longer a subtype of Rectangle — both implement a small immutable Shape interface with just area(), and birds split into a Bird base and a separate FlyingBird capability. Run every substitute through the bench again: they all pass green. Note what changed — we replaced bad inheritance with a shared interface (and composition), so no substitute can break a promise it never made.
In practice
When to use it — and what trips people up
How to obey LSP — and spot when inheritance is wrong
Before you write extends or : public, ask: can this subtype keep every promise the base makes, in every method, with no special-casing by callers? If yes, inheritance is a good fit. If you can already imagine a method the subtype would have to weaken, throw out of, or no-op, stop — inheritance is the wrong tool here.
- Reach for a shared interface when several types offer the same capability but don't share real behaviour.
RectandSqbotharea(), but neither needs the other's setters — an immutableShapeinterface fits perfectly. - Reach for composition when a type has a behaviour rather than is a kind of something. A
Penguinhas-aswim()ability; aSparrowhas-afly()ability. Model the capability as a separate piece you compose in, not a method on a one-size base. - Make objects immutable when you can. Half of LSP traps (the Rectangle/Square one included) come from mutating setters. No setters, no setter-promise to break.
- Watch the callers. If using a subtype forces
if (x instanceof Subtype)checks anywhere, substitutability is already broken — let that be your alarm.
When the subtype can't honour the full contract
Prefer composition or a shared interface over inheritance whenever a subtype can't keep every promise of the base. “Favour composition over inheritance” is, in large part, just LSP advice in disguise — it steers you away from is-a trees that can't honour their contracts.
What it gives you
- Callers stay simple and correct — code written against the base needs no
instanceofchecks or special cases for particular subtypes. - Subtypes become freely swappable, which is exactly what makes polymorphism, mocking, and dependency injection trustworthy.
- Bugs surface as design questions early ("can this really stand in for the base?") instead of as runtime surprises in production.
- It keeps inheritance trees honest — you only inherit where behaviour truly transfers, so the hierarchy actually means something.
Common mistakes
- It takes discipline and judgement: the violations (preconditions, postconditions, hidden exceptions) are subtle and easy to miss in review.
- Honouring the full contract can mean more types — separate interfaces or composed capabilities instead of one tidy base class.
- Real-world is-a relationships (square/rectangle, penguin/bird) tempt you into inheritance that violates LSP, so intuition can mislead.
- Languages give you little help — most won't warn you when an override strengthens a precondition or throws a new exception; the check stays manual.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
// ✗ VIOLATION: Square "is-a" Rectangle on paper, but doesn't behave like one.
class Rectangle {
constructor(protected w = 0, protected h = 0) {}
setWidth(w: number) { this.w = w; }
setHeight(h: number) { this.h = h; }
area() { return this.w * this.h; }
}
class Square extends Rectangle {
setWidth(w: number) { this.w = w; this.h = w; } // also changes height!
setHeight(h: number) { this.w = h; this.h = h; } // also changes width!
}
// A caller written for the BASE type:
function checkArea(r: Rectangle) {
r.setWidth(5);
r.setHeight(4);
return r.area() === 20; // promise: 5 × 4 == 20
}
checkArea(new Rectangle()); // true ✓
checkArea(new Square()); // false ✗ — area is 16, the Square broke LSP
// ✓ FIX: don't inherit. Share an immutable interface; each shape computes its own area.
interface Shape { area(): number; }
class Rect implements Shape {
constructor(private readonly w: number, private readonly h: number) {}
area() { return this.w * this.h; }
}
class Sq implements Shape {
constructor(private readonly side: number) {}
area() { return this.side * this.side; }
}
// Now ANY Shape substitutes safely — there's no setter promise to break.References & further reading
5 sources- Docsen.wikipedia.org
Liskov substitution principle — Wikipedia
The canonical overview: the formal substitutability definition, the precondition/postcondition rules, and the very Square-extends-Rectangle example used in this lesson.
- Paper
Data Abstraction and Hierarchy — Barbara Liskov (1987)
The original SIGPLAN keynote where Liskov introduced the substitution idea. Worth reading once to see the principle in the author's own words; widely available as a PDF by searching the title.
- Articleblog.cleancoder.com
Solid Relevance — Robert C. Martin (Uncle Bob)
Uncle Bob, who coined the SOLID acronym, argues LSP is about keeping abstractions crisp — "a program that uses an interface must not be confused by an implementation of that interface." A short, opinionated read.
- Articlestackify.com
The Liskov Substitution Principle, with code examples — Stackify
A friendly, example-led walkthrough that builds the violation and the fix step by step. A good second read right after this lesson.
- Talkyoutube.com
Liskov Substitution Principle (SOLID), Robustness & Design by Contract — Code Walks
A clear talk that ties LSP to the Robustness Principle and Design by Contract — exactly the preconditions/postconditions framing used in the rule-of-thumb tests.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Sign in to save your best score.
question 01 / 05
In one sentence, what does the Liskov Substitution Principle require?
question 02 / 05
A caller does setWidth(5); setHeight(4) and expects area == 20. Why does Square extends Rectangle break it?
question 03 / 05
You override a base method only to throw new UnsupportedOperationException() (like Penguin.fly()). What does this signal?
question 04 / 05
Which of these is NOT one of the LSP rule-of-thumb tests for a valid override?
question 05 / 05
Your Square can't honour Rectangle's independent-setter contract. What's the cleaner fix?
0/5 answered