Loading learning content...
Picture this scenario: You've built a robust payment processing system that works beautifully with CreditCardPayment objects. The system validates cards, processes transactions, handles refunds, and logs everything correctly. Your code treats every payment uniformly through a Payment base class interface.
Then a colleague introduces CryptocurrencyPayment as a subclass of Payment. It inherits from the same base class, so it should "just work," right? You deploy to production. Within hours, your refund system crashes because cryptocurrency payments throw UnsupportedOperationException on refunds. Your validation logic fails silently because crypto payments ignore the validation method entirely. Your logging breaks because crypto payments return null for transaction IDs.
Your system trusted that subclasses would behave like the base class. That trust was violated.
This is exactly the kind of failure the Liskov Substitution Principle (LSP) is designed to prevent.
By the end of this page, you will understand LSP's core definition—what substitutability means, why it matters, and how violations create subtle yet devastating bugs. You'll learn to think about inheritance not as code reuse, but as a behavioral contract between types.
The Liskov Substitution Principle can be stated simply:
If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program.
Let's unpack what this really means. The principle demands that anywhere your code expects a Parent type, you should be able to pass any Child type without the code breaking, behaving unexpectedly, or requiring special handling.
Here's a practical test for LSP: If you have to check instanceof or the specific subtype to make your code work correctly, you're likely violating LSP. The whole point is that the caller shouldn't need to know which specific subtype they're dealing with.
What "substitutable" actually means:
Substitutability isn't just about syntax—it's about behavior. A subclass is truly substitutable only if it:
When any of these conditions are violated, code that works with the parent type will fail when given the subclass—often in subtle, hard-to-diagnose ways.
12345678910111213141516171819202122232425262728293031323334353637383940
// Base class establishes a contractabstract class Bird { abstract fly(): void; abstract makeSound(): string;} // Sparrow is fully substitutable - it honors all contractsclass Sparrow extends Bird { fly(): void { console.log("Sparrow flies through the air"); } makeSound(): string { return "Chirp chirp!"; }} // Penguin violates LSP - it cannot fulfill the fly() contractclass Penguin extends Bird { fly(): void { // VIOLATION: Throws instead of flying throw new Error("Penguins cannot fly!"); } makeSound(): string { return "Honk!"; }} // Client code written against the base classfunction releaseBirds(birds: Bird[]): void { for (const bird of birds) { // This code TRUSTS that all Birds can fly bird.fly(); // Crashes when bird is a Penguin! }} // The problem: Penguin IS-A Bird syntactically, but NOT behaviorallyconst flock: Bird[] = [new Sparrow(), new Penguin()];releaseBirds(flock); // Runtime explosionThe Bird/Penguin example is classic because it reveals a fundamental truth: inheritance should model behavioral compatibility, not just taxonomic classification.
In biology, penguins are indeed birds. But in software, inheritance implies more than classification—it implies that the child can do everything the parent can do. A Penguin that throws an exception when asked to fly breaks this contract.
What's the big deal if one subclass doesn't quite fit? The consequences ripple through your entire codebase in ways that are often invisible until they cause production incidents.
The trust problem:
When code is written against an abstraction (a base class or interface), it makes assumptions about behavior. These assumptions are implicit contracts:
save(), the data will be persistedvalidate(), I'll get a boolean indicating validitycalculate(), I'll get a numeric result within expected boundsEvery piece of code that uses the abstraction depends on these contracts. When a subclass violates them, it doesn't just break one function—it potentially breaks every function that uses that abstraction.
| Violation Type | Immediate Effect | Cascading Consequences |
|---|---|---|
| Method throws unexpected exception | Caller crashes without handling | Uncaught exceptions propagate up the stack; system instability |
| Method silently does nothing | Operation appears successful but isn't | Data corruption; inconsistent state; debugging nightmares |
| Method returns unexpected value/type | Caller logic operates on bad data | Wrong decisions propagate; subtle calculation errors |
| Method has side effects parent doesn't have | Unexpected mutations occur | Race conditions; resource leaks; security vulnerabilities |
| Method requires preconditions parent doesn't | Caller fails to provide them | Random failures depending on input; intermittent bugs |
LSP violations often don't cause immediate crashes. They cause wrong behavior that appears correct. Your system continues running, but it's making wrong decisions, corrupting data, or creating technical debt invisibly. These are the bugs that take weeks to diagnose.
The polymorphism foundation:
Polymorphism—the ability to write code that works with any object in a type hierarchy—is one of object-oriented programming's greatest powers. But polymorphism only works if substitutability is guaranteed.
Consider this chain of logic:
ShapeCircle, Rectangle, Triangle without changing my codeThis beautiful property collapses the moment one shape violates the contract. If InvalidShape throws exceptions or returns null where others return values, my generic code breaks. I'm forced to add instanceof checks. Polymorphism degrades into switch statements. The architecture unravels.
A critical distinction: Your compiler or type checker verifying that Child extends Parent is not the same as LSP compliance. LSP is about behavioral compatibility, not syntactic compatibility.
The compiler checks:
LSP additionally requires:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// TYPE-COMPATIBLE but NOT LSP-COMPLIANT interface Stack<T> { push(item: T): void; pop(): T | undefined; peek(): T | undefined; size(): number;} // This implementation is type-compatibleclass LimitedStack<T> implements Stack<T> { private items: T[] = []; private readonly maxSize: number = 10; push(item: T): void { // VIOLATION: Silently ignores items beyond limit // Parent contract implies push always adds the item if (this.items.length < this.maxSize) { this.items.push(item); } // No error, no indication - item just disappears } pop(): T | undefined { return this.items.pop(); } peek(): T | undefined { return this.items[this.items.length - 1]; } size(): number { return this.items.length; }} // Client code that trusts the Stack contractfunction processItems<T>(stack: Stack<T>, items: T[]): void { for (const item of items) { stack.push(item); } console.log(`Pushed ${items.length} items, stack size: ${stack.size()}`);} // Works as expected with regular stack// With LimitedStack: "Pushed 100 items, stack size: 10" - DATA LOSS!The LimitedStack compiles perfectly. TypeScript, Python, Java—none of them will catch this bug. The violation is behavioral, not syntactic. This is why LSP requires programmer discipline beyond what any type system can verify.
The fix requires behavioral alignment:
If LimitedStack can't accept all items, it must signal this clearly:
BoundedStack) that documents the limitThe key is that client code must not be surprised. Either the subclass behaves exactly like the parent, or it uses a different type that communicates its different behavior.
LSP violations often stem from a fundamental mistake: using inheritance for reasons other than behavioral compatibility.
Common misuses of inheritance:
The right reason to inherit:
Inheritance should be used when and only when the child genuinely is a specialization of the parent in terms of behavior. The child should:
Let's examine a properly designed hierarchy:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
// Well-designed: ReadOnlyCollection is genuinely substitutableinterface Collection<T> { size(): number; contains(item: T): boolean; iterator(): Iterator<T>;} // ReadOnlyList IS-A Collection - fully substitutableinterface ReadOnlyList<T> extends Collection<T> { get(index: number): T; indexOf(item: T): number;} // Separate interface for mutation - doesn't force immutable types to lieinterface MutableCollection<T> extends Collection<T> { add(item: T): boolean; remove(item: T): boolean; clear(): void;} // ArrayList implements both - it can do everything both contracts requireclass ArrayList<T> implements ReadOnlyList<T>, MutableCollection<T> { private items: T[] = []; size(): number { return this.items.length; } contains(item: T): boolean { return this.items.includes(item); } get(index: number): T { return this.items[index]; } indexOf(item: T): number { return this.items.indexOf(item); } add(item: T): boolean { this.items.push(item); return true; } remove(item: T): boolean { const idx = this.items.indexOf(item); if (idx >= 0) { this.items.splice(idx, 1); return true; } return false; } clear(): void { this.items = []; } iterator(): Iterator<T> { return this.items[Symbol.iterator](); }} // ImmutableList implements ONLY ReadOnlyList - honest about its capabilitiesclass ImmutableList<T> implements ReadOnlyList<T> { private readonly items: readonly T[]; constructor(items: T[]) { this.items = [...items]; } size(): number { return this.items.length; } contains(item: T): boolean { return this.items.includes(item); } get(index: number): T { return this.items[index]; } indexOf(item: T): number { return this.items.indexOf(item); } iterator(): Iterator<T> { return this.items[Symbol.iterator](); } // No mutation methods - ImmutableList doesn't pretend to be mutable} // Both are safely substitutable for ReadOnlyList!function countLargeItems<T>(list: ReadOnlyList<T>, predicate: (t: T) => boolean): number { let count = 0; const iter = list.iterator(); let result = iter.next(); while (!result.done) { if (predicate(result.value)) count++; result = iter.next(); } return count;}Notice how ImmutableList doesn't implement MutableCollection and pretend mutations work. It only claims to be what it truly is. This is LSP-compliant design: types only promise what they can deliver.
How do you know if your hierarchy has substitutability problems? Watch for these warning signs in your codebase:
instanceof checks before method calls — If you need to know the concrete type to call a method safely, substitutability is brokennull, -1, or empty results where parent returns meaningful data1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
// SMELL 1: instanceof before method callfunction processDocument(doc: Document): void { // If you need to check the type, LSP is violated if (doc instanceof PdfDocument) { doc.renderWithPdfRenderer(); } else if (doc instanceof WordDocument) { doc.renderWithWordRenderer(); } else { // What about new document types? throw new Error("Unknown document type"); }} // SMELL 2: Empty method overrideclass ReadOnlyRepository extends Repository { override save(entity: Entity): void { // Does nothing - violates parent contract } override delete(id: string): void { throw new Error("Cannot delete from read-only repository"); }} // SMELL 3: Type flags controlling behaviorclass Shape { supportsRotation: boolean = true; rotate(degrees: number): void { if (!this.supportsRotation) { throw new Error("Rotation not supported"); } // Actual rotation logic }} class Line extends Shape { constructor() { super(); this.supportsRotation = false; // Reveals LSP violation }} // SMELL 4: Try-catch around polymorphic callsfunction exportAllShapes(shapes: Shape[]): void { for (const shape of shapes) { try { shape.export(); // Some shapes throw here } catch (e) { console.log(`Skipping ${shape.constructor.name}: export not supported`); } }}Each of these smells indicates the same underlying problem: the inheritance hierarchy doesn't represent true behavioral compatibility. The code is forced to work around the fact that not all subclasses are truly substitutable.
When you see these patterns, it's usually a sign that you need either (1) a different hierarchy where subtypes don't inherit capabilities they don't have, (2) smaller, more focused interfaces that subtypes can honestly implement, or (3) composition instead of inheritance.
We've established the foundational understanding of what LSP means and why it matters. Let's crystallize the key insights:
instanceof, empty overrides, defensive try-catch blocks all signal violationsWhat's next:
We've established the conceptual foundation. The next page dives into Barbara Liskov's original formulation of this principle—the precise, rigorous definition that computer scientists use. Understanding her formulation will give you the analytical tools to reason about substitutability with precision.
You now understand LSP's core definition: subclasses must be fully substitutable for their base classes. This isn't about satisfying a type checker—it's about ensuring behavioral compatibility that makes polymorphism work. Next, we'll explore Barbara Liskov's formal articulation of this principle.