Last Updated: February 13, 2026
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 are 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. You extend the Document class and override save() to block writes.
Seems reasonable, right?
But Then Reality Hits…
Let's see how this plays out in client code. A DocumentProcessor class takes any Document and tries to process and save it.
Output:
The client code expected any Document to be savable. But when it received a ReadOnlyDocument, that assumption exploded into a runtime exception.
At the heart of this failure is a violation of a fundamental design principle: the 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, you are likely violating LSP.
ReadOnlyDocument inherits save() from Document but throws an exception instead of saving, breaking the contract that any Document should be saveable.
"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.
Subtypes must honor the expectations set by their base types. The client code should not need to know or care which specific subtype it is dealing with. Everything should "just work."
When LSP is followed, your code behaves consistently. You can substitute any subtype and still get the behavior your client code expects. No unpleasant surprises.
LSP violations often lead to conditional logic (e.g., if (obj instanceof ReadOnlyDocument)) in client code to handle subtypes differently. This is a code smell. It is a sign that your design is leaking abstraction. When client code has to "know" the subtype to behave correctly, you have broken polymorphism.
Well-behaved hierarchies are easier to understand, maintain, and extend. You can add new subtypes without fear of breaking existing code that relies on the base type's contract.
LSP is what makes polymorphism truly powerful. You can write generic algorithms that operate on a base type, confident that they will work correctly with any current or future subtype.
Tests written for the base class's interface should, in theory, pass for all its subtypes if LSP is respected. This means you can reuse test suites and trust that your inheritance hierarchies are well-formed.
Essentially, LSP helps you build systems that are easier to extend, less prone to bugs, and far more resilient to change.
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:
The Document interface represents read-only access: the ability to open and view data. Editable extends Document, adding the ability to save. Anything Editable is automatically a Document too, but not every Document is Editable. This clearly defines what each object can do, and prevents clients from assuming editability unless explicitly promised.
EditableDocument and ReadOnlyDocumentNow we implement our two concrete types. The EditableDocument implements Editable (which already includes everything from Document), while the ReadOnlyDocument implements only Document.
Now both types are valid Document objects. EditableDocument implements Editable (which includes Document), while ReadOnlyDocument implements only Document. There is no risk of calling save() on a read-only document, because it simply is not part of its contract.
Now the design is clean. Editable extends Document, so EditableDocument gets both reading and writing through a single interface. ReadOnlyDocument implements only Document. A ReadOnlyDocument never promises save() functionality, so it can never break that contract.
Now let's update DocumentProcessor to work with these new interfaces. Since Editable extends Document, the processAndSave method simply accepts an Editable parameter.
The processor has two methods: process() accepts any Document for reading, and processAndSave() accepts an Editable, which guarantees both reading and writing capabilities. If you try to pass a ReadOnlyDocument to processAndSave(), the compiler rejects it before the program ever runs.
Notice there is no instanceof check or runtime type verification anywhere. Because Editable extends Document, the processAndSave method accepts an Editable and gets access to both open()/getData() and save() through a single parameter.
The type system itself prevents you from passing a ReadOnlyDocument to processAndSave(). If you try, the compiler rejects it. This is the real power of the LSP-compliant design: correctness is enforced at compile time, not discovered at runtime through exceptions.
Here is a usage example showing the refactored code in action.
Understanding the Liskov Substitution Principle is one thing. Applying it correctly in the real world is 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 does not mean it is 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. A penguin might be a bird in biology, but if your Bird interface promises flight, then Penguin is not a valid subtype.
If you find yourself writing code like this:
That is a clear warning sign. If a subclass cannot meaningfully implement a method defined in the base class, it is likely not a valid subtype. This leads to brittle code and runtime surprises, which is 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 have violated the principle of substitution. Polymorphism should make the client code unaware of specific subtypes. If you are relying on instanceof, it is time to revisit your abstraction.
Subclasses should not arbitrarily tighten or loosen the behavior defined by the base class. For example, making a mutable property in the base class immutable in the subclass (or vice versa) can lead to subtle bugs. Changing validation logic in ways that break existing assumptions in client code is another LSP violation.
Consistency is key. If the base type makes a promise, every subtype must keep it.
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.