Have you ever passed a subclass into a method expecting the parent class… and watched your program crash or behave in unexpected ways?
Or extended a class… only to find yourself overriding methods just to throw exceptions?
If yes, you’ve probably run into a violation of one of the most misunderstood object-oriented design principles: The Liskov Substitution Principle (LSP).
Let’s understand it with a real-world example and why it breaks LSP.
Imagine you're building a system to manage different types of documents.
You start with a simple base class:
Now, a new requirement comes in:
“We need a read-only document type—for sensitive content like government reports or signed contracts.”
You think: A ReadOnlyDocument is still a kind of Document, so inheritance makes sense.
So, you extend the Document
class:
Seems reasonable, right?
But Then Reality Hits…
Let’s see how this plays out in client code:
Output:
Boom!The client code expected any Document
to be savable. But when it received a ReadOnlyDocument
, that assumption exploded into a runtime exception.
What Went Wrong?
At the heart of this failure is a violation of a fundamental design principle: Liskov Substitution Principle.
Our subtype (ReadOnlyDocument
) cannot be seamlessly substituted for its base type (Document
) without altering the desired behavior of the program.
If you ever find yourself overriding a method just to throw an exception, or adding subtype-specific conditions in client code—it’s a red flag and you might be violating LSP.
"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 that program (correctness, task performed, etc.)." — Barbara Liskov, 1987
In simpler terms: If a class S
extends or implements class T
, then you should be able to use S
anywhere T
is expected—without breaking the program’s behavior or logic.
In other words, subtypes must honor the expectations set by their base types. The client code shouldn’t need to know or care which specific subtype it’s dealing with. Everything should “just work.”
Essentially, LSP helps you build systems that are:
Let’s refactor our design so that subtypes like ReadOnlyDocument
can be used without violating the expectations set by the base type.
The root problem was that the base class Document
assumed all documents are editable, but not all documents should be. To fix this, we need to:
Instead of having one base class with assumptions about mutability, let’s break responsibilities apart:
Document
: represents the ability to open and view dataEditable
: represents the capability to modify dataThis clearly defines what each object can do—and prevents clients from assuming editability unless explicitly promised.
EditableDocument
and ReadOnlyDocument
Now we implement our two concrete types:
EditableDocument
ReadOnlyDocument
Now:
EditableDocument
and ReadOnlyDocument
are valid Document
objectsEditableDocument
implements the Editable
interfacesave()
on a read-only document—it’s simply not possibleLet’s update DocumentProcessor
to act accordingly:
Alternatively, use two different methods:
Understanding the Liskov Substitution Principle is one thing. Applying it correctly in the real world—that's where the challenges begin.
Here are some of the most common traps to watch out for:
Just because something sounds like it “is a” something else in natural language doesn’t mean it’s a valid subtype in code.
Take this classic example:
A penguin is a bird, but penguins can’t fly.If your Bird
class has a fly()
method, and you override it in Penguin
to throw an exception or do nothing—you've violated LSP.
The key insight: subtyping must be based on behavior, not just taxonomy.
If you find yourself writing code like this:
That’s a flashing red warning light. If a subclass cannot meaningfully implement a method defined in the base class, it’s likely not a valid subtype.
This leads to brittle code and runtime surprises—exactly what LSP aims to prevent.
Changing the assumptions of a method is a subtle but dangerous LSP violation.
These break the trust that clients place in the base class’s behavior.
Code like this is often a symptom of broken design:
Whenever client code has to know the exact subtype to behave correctly, you’ve violated the principle of substitution.
Polymorphism should make the client code unaware of specific subtypes. If you’re relying on instanceof
, it’s time to revisit your abstraction.
Subclasses shouldn’t arbitrarily tighten or loosen the behavior defined by the base class.
For example:
Consistency is key.
Yes, but it’s more precise.LSP defines what correct behavioral inheritance actually looks like. It’s not just about reusing code, it’s about preserving correctness and intention.
Think of LSP as a safety net for polymorphism. It ensures your abstractions can scale and evolve cleanly.
This is exactly when you should stop and rethink your hierarchy.
Some options:
ReadOnlyDocument
that can't be saved probably shouldn't inherit from a Document
class that supports saving.Readable
, Editable
, etc., to model capabilities explicitly.instanceof
or casting?Not never, but be cautious.
There are legitimate, narrow use cases: implementing equals()
, serialization, certain framework hooks.
But if you’re using instanceof
to drive business logic or alter behavior, you’re likely covering up an LSP violation.
Ask yourself:
“Am I using this because I broke polymorphism?”
If yes, revisit your design.