So far in this series, we've explored two of the SOLID principles.
- Single Responsibility Principle (SRP) taught us to design classes with one clear responsibility.
- Open/Closed Principle (OCP) showed us how to extend software without constantly modifying existing, stable code.
Now it's time to look at what is often considered the most misunderstood SOLID principle:
The Liskov Substitution Principle (LSP).
Its formal definition is:
Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
That's... a mouthful.
The definition is technically correct, but it doesn't tell us why this principle exists.
So, like every article in this series, let's start with a problem.
The Problem
Imagine we're building a payment system.
Every payment method should support refunds.
So we create a base class.
abstract class PaymentMethod {
abstract pay(amount: number): void;
abstract refund(amount: number): void;
}
Now we implement different payment methods.
class CreditCardPayment extends PaymentMethod {
pay(amount: number) {
console.log("Payment successful.");
}
refund(amount: number) {
console.log("Refund successful.");
}
}
Everything works perfectly.
Later, the business introduces Cash on Delivery (COD).
Naturally, we extend the same parent class.
class CashOnDeliveryPayment extends PaymentMethod {
pay(amount: number) {
console.log("Customer will pay on delivery.");
}
refund(amount: number) {
throw new Error("Refunds are not supported.");
}
}
The application compiles.
The inheritance hierarchy looks correct.
But do we actually have a valid substitute for PaymentMethod?
Not really.
What goes wrong?
Somewhere else in the application, another developer writes:
function cancelOrder(payment: PaymentMethod) {
payment.refund(500);
}
This function doesn't care whether it's dealing with:
- Credit Card
- UPI
- PayPal
It simply assumes:
Every PaymentMethod supports refunds.
That assumption was true...
Until CashOnDeliveryPayment appeared.
Now the application crashes.
The problem isn't the function.
The problem is that one child class broke the expectations established by its parent.
The Real Meaning of LSP
Liskov Substitution Principle asks a simple question:
If I replace a parent object with one of its children, should the rest of the program continue to work correctly?
If the answer is no, the inheritance relationship is probably incorrect.
The child isn't truly behaving like the parent promised.
Another Famous Example
Perhaps you've seen this example before.
Imagine we model shapes like this.
Shape
├── Rectangle
└── Square
It sounds perfectly reasonable.
After all...
A square is a rectangle.
Right?
Let's see.
A rectangle allows width and height to change independently.
rectangle.setWidth(10);
rectangle.setHeight(5);
Now imagine Square.
Whenever we change the width, we must also change the height.
square.setWidth(10);
// Height automatically becomes 10
Now consider this function.
function resizeRectangle(rectangle: Rectangle) {
rectangle.setWidth(10);
rectangle.setHeight(5);
console.log(rectangle.getArea());
}
If we pass a normal rectangle...
Area = 50
If we pass a square...
Area = 100
The function behaves differently because the child changed the expectations established by the parent.
The inheritance relationship looked correct mathematically.
It wasn't correct from a software design perspective.
The Root Problem
The issue isn't inheritance.
The issue is a bad abstraction.
Inheritance creates expectations.
Whenever we inherit from a parent class, we're making a promise.
We're saying:
You can use me anywhere you use my parent.
If that promise isn't true...
Inheritance becomes dangerous.
Other developers begin making assumptions - that aren't always valid.
Those assumptions eventually become bugs.
LSP Is About Behaviour, Not Hierarchy
One of the biggest misconceptions is thinking LSP is about inheritance syntax.
It isn't.
The compiler only checks whether a child inherits correctly.
LSP asks something deeper.
Does the child preserve the behaviour clients expect?
Two classes may have identical methods.
They may even share the same parent.
Yet one may still violate LSP if it behaves differently in unexpected ways.
Behaviour matters more than hierarchy.
How to Spot an LSP Violation
Here are a few warning signs.
- A child throws
UnsupportedOperationExceptionor similar errors for inherited methods. - A child ignores methods defined by the parent.
- Client code starts checking object types before calling methods.
For example:
if (payment instanceof CashOnDeliveryPayment) {
// Skip refund
}
Whenever you find yourself writing lots of instanceof checks for subclasses, it's often a sign that the inheritance hierarchy isn't modelling the problem correctly.
A Better Design
Instead of forcing every payment method to support refunds, separate the capabilities.
interface PaymentMethod {
pay(amount: number): void;
}
interface Refundable {
refund(amount: number): void;
}
Now:
- Credit Card implements both.
- PayPal implements both.
- Cash on Delivery only implements
PaymentMethod.
No fake implementations.
No unexpected exceptions.
No broken promises.
The Key Takeaway
The Liskov Substitution Principle teaches us that inheritance isn't just about sharing code.
It's about preserving expectations.
Whenever a child class replaces its parent, the rest of the application should continue to work without knowing the difference.
If client code needs special cases for certain subclasses, or if some subclasses can't persist the behaviour promised by the parent, the inheritance hierarchy probably needs another look.
What's Next?
We've now covered three SOLID principles.
- SRP helped us design focused classes.
- OCP taught us to extend behaviour without modifying stable code.
- LSP reminded us that inheritance should preserve behaviour, not just structure.
Next, we'll explore the Interface Segregation Principle (ISP).
We'll answer another practical question:
Why is forcing classes to implement methods they don't need considered a design problem?

Top comments (0)