Liskov Substitution Principle (LSP)
1. What is Liskov Substitution Principle?
Liskov Substitution Principle (LSP) is the third principle of SOLID.
It states:
Objects of a derived class should be able to replace objects of the base class without breaking the application or changing the expected behavior.
In simple English:
If a child class inherits from a parent class, the child should behave in a way that the parent promises.
Or even simpler:
Child should correctly replace Parent.
2. Why do we need LSP?
Inheritance allows us to write:
Parent obj = new Child();
This is perfectly valid C#.
But just because the code compiles doesn't mean the design is correct.
Consider:
Bird
│
├── Sparrow
└── Penguin
If the base Bird class says:
Every Bird can Fly()
then:
Sparrow → Fly() ✓
Penguin → Fly() ✗
A penguin is a bird, but it cannot fly.
If Penguin.Fly() throws an exception, code expecting a normal Bird may break.
That is an LSP violation.
3. Main Idea
Suppose:
Parent
↑
│
Child
If this works:
Parent obj = new Parent();
obj.DoWork();
then replacing it with:
Parent obj = new Child();
obj.DoWork();
should still satisfy the expectations of the Parent contract.
Conceptually:
Parent expected
↓
Child provided
↓
Application still behaves correctly
↓
✓ LSP
4. Simple Scenario — Bird Application
Let's first create a program that violates LSP.
We have:
Bird
├── Sparrow
└── Penguin
We assume all birds can:
Fly()
That assumption is the problem.
5. Before LSP — Bad Design
using System;
public abstract class Bird
{
public abstract void Fly();
}
public class Sparrow : Bird
{
public override void Fly()
{
Console.WriteLine("Sparrow is flying.");
}
}
public class Penguin : Bird
{
public override void Fly()
{
throw new NotSupportedException(
"Penguin cannot fly.");
}
}
public class BirdService
{
public void MakeBirdFly(Bird bird)
{
bird.Fly();
}
}
public class Program
{
public static void Main()
{
BirdService service = new BirdService();
Bird sparrow = new Sparrow();
service.MakeBirdFly(sparrow);
Bird penguin = new Penguin();
service.MakeBirdFly(penguin);
}
}
Output
Sparrow is flying.
Unhandled exception:
System.NotSupportedException:
Penguin cannot fly.
6. What is Wrong Here?
Look at this method:
public void MakeBirdFly(Bird bird)
{
bird.Fly();
}
BirdService accepts:
Bird bird
So naturally it assumes:
If you give me a
Bird, I can callFly().
This works:
Bird bird = new Sparrow();
service.MakeBirdFly(bird);
But this fails:
Bird bird = new Penguin();
service.MakeBirdFly(bird);
Therefore:
Bird expected
↓
Sparrow supplied
↓
Fly()
↓
Works ✓
But:
Bird expected
↓
Penguin supplied
↓
Fly()
↓
Exception ✗
The Penguin cannot safely substitute for the Bird abstraction as it was designed.
That violates LSP.
7. Important Point
The problem is not that Penguin is a bad class.
The problem is our abstraction:
public abstract class Bird
{
public abstract void Fly();
}
We made an incorrect assumption:
Every bird can fly.
The abstraction is too broad.
LSP often exposes this type of incorrect inheritance design.
8. How Do We Fix It?
We separate:
What every bird can do
from:
What only flying birds can do
For example:
Bird
↑
┌─────┴─────┐
│ │
Sparrow Penguin
IFlyingBird
↑
│
Sparrow
Now:
- Sparrow is a bird.
- Penguin is a bird.
- Sparrow can fly.
- Penguin is not forced to support flying.
9. Full Simple Program Following LSP
using System;
public abstract class Bird
{
public abstract void Eat();
}
public interface IFlyingBird
{
void Fly();
}
public class Sparrow : Bird, IFlyingBird
{
public override void Eat()
{
Console.WriteLine("Sparrow is eating.");
}
public void Fly()
{
Console.WriteLine("Sparrow is flying.");
}
}
public class Penguin : Bird
{
public override void Eat()
{
Console.WriteLine("Penguin is eating.");
}
}
public class BirdService
{
public void FeedBird(Bird bird)
{
bird.Eat();
}
public void MakeBirdFly(IFlyingBird bird)
{
bird.Fly();
}
}
public class Program
{
public static void Main()
{
BirdService service = new BirdService();
Bird sparrow = new Sparrow();
Bird penguin = new Penguin();
service.FeedBird(sparrow);
service.FeedBird(penguin);
IFlyingBird flyingBird = new Sparrow();
service.MakeBirdFly(flyingBird);
}
}
Output
Sparrow is eating.
Penguin is eating.
Sparrow is flying.
No exception.
10. Program Explanation
Let's understand each part.
Step 1 — Bird
public abstract class Bird
{
public abstract void Eat();
}
Now the Bird abstraction contains behavior that is valid for both:
Sparrow
Penguin
Both can eat.
Therefore:
Bird
│
└── Eat()
is a reasonable abstraction for our simple example.
11. IFlyingBird
public interface IFlyingBird
{
void Fly();
}
Flying is now a separate capability.
Only birds that can fly implement this interface.
IFlyingBird
│
└── Fly()
12. Sparrow
public class Sparrow : Bird, IFlyingBird
{
public override void Eat()
{
Console.WriteLine("Sparrow is eating.");
}
public void Fly()
{
Console.WriteLine("Sparrow is flying.");
}
}
A sparrow:
- Is a
Bird - Can
Eat() - Is an
IFlyingBird - Can
Fly()
Therefore:
Bird
↑
│
Sparrow
│
↓
Eat()
IFlyingBird
↑
│
Sparrow
│
↓
Fly()
13. Penguin
public class Penguin : Bird
{
public override void Eat()
{
Console.WriteLine("Penguin is eating.");
}
}
Penguin inherits:
Bird
because it can satisfy the Bird behavior used in this design.
But it does not implement:
IFlyingBird
Therefore, we don't need ugly code like:
throw new NotSupportedException();
for Fly().
14. BirdService
public class BirdService
{
public void FeedBird(Bird bird)
{
bird.Eat();
}
public void MakeBirdFly(IFlyingBird bird)
{
bird.Fly();
}
}
Notice the parameter types.
For eating:
public void FeedBird(Bird bird)
It accepts any Bird.
Therefore:
service.FeedBird(new Sparrow());
works.
And:
service.FeedBird(new Penguin());
also works.
Both satisfy the expected Bird behavior.
15. Flying Method
For flying:
public void MakeBirdFly(IFlyingBird bird)
It doesn't accept every Bird.
It specifically requires:
IFlyingBird
Therefore:
service.MakeBirdFly(new Sparrow());
works because Sparrow implements IFlyingBird.
But this:
service.MakeBirdFly(new Penguin());
doesn't compile.
And that is actually good.
The compiler prevents us from passing an object that doesn't support the required capability.
16. Before vs After LSP
Before
Bird
│
Fly()
↑
┌─────┴─────┐
│ │
Sparrow Penguin
│ │
↓ ↓
Flying Cannot Fly
✓ ✗
Penguin is forced to implement something it cannot correctly support.
After
Bird
│
Eat()
↑
┌─────┴─────┐
│ │
Sparrow Penguin
✓ ✓
IFlyingBird
│
Fly()
↑
│
Sparrow
✓
Now each abstraction makes a promise that its implementations can actually satisfy.
17. LSP is More Than "Don't Throw Exceptions"
This is important for interviews.
LSP doesn't simply mean:
Child classes shouldn't throw
NotSupportedException.
The deeper rule is:
A subtype must respect the behavioral contract of its parent/base abstraction.
For example, imagine:
public abstract class PaymentService
{
public abstract bool ProcessPayment(decimal amount);
}
Callers may expect:
Valid amount
↓
Process payment
↓
Return true/false
Now imagine one child implementation unexpectedly says:
I don't support amounts above ₹1,000
even though the base contract permits them.
The subtype has introduced a stronger restriction.
That can violate substitutability.
18. LSP and Method Contracts
A useful way to understand LSP is through contracts.
Suppose a parent says:
Input accepted:
Amount > 0
Output:
Payment result
A child shouldn't suddenly say:
Input accepted:
Amount > 10,000 only
because the child has made the requirement stronger.
A caller that works with the parent may pass:
₹5,000
and reasonably expect it to work.
But the child rejects it.
That breaks substitutability.
19. Preconditions
A precondition is something that must be true before a method executes.
Example:
ProcessPayment(amount)
Base contract:
amount > 0
A child should not unexpectedly require:
amount >= 10,000
because it has made the precondition stronger.
Simplified rule:
A child shouldn't demand more from the caller than the parent contract demands.
20. Postconditions
A postcondition describes what the method promises after execution.
Suppose the base contract promises:
Save()
↓
Record is persisted successfully
A derived implementation shouldn't return success while not actually persisting the record.
Simplified:
A child shouldn't provide less than what the parent promises.
21. Easy Way to Understand LSP
Remember this sentence:
Don't surprise the caller.
If a method expects:
Bird
then every valid subtype of Bird should satisfy the behavior promised by Bird.
The caller shouldn't need code like:
if (bird is Penguin)
{
// Don't call Fly()
}
else
{
bird.Fly();
}
This is often a warning that the abstraction or inheritance hierarchy needs reconsideration.
22. Another Real-World Example — Payment
Consider:
IPayment
↑
┌────────┼────────┐
│ │ │
CreditCard UPI PayPal
Suppose IPayment promises:
ProcessPayment()
Then each implementation should honor that contract.
CreditCardPayment
↓
ProcessPayment() ✓
UPIPayment
↓
ProcessPayment() ✓
PayPalPayment
↓
ProcessPayment() ✓
Then the application can work with:
IPayment payment;
without caring about the exact implementation.
This is the kind of substitutability LSP encourages.
23. LSP and Polymorphism
LSP is strongly connected with polymorphism.
Polymorphism allows:
Bird bird = new Sparrow();
or:
Bird bird = new Penguin();
LSP asks:
Do these substitutions actually behave correctly according to the Bird contract?
So:
Polymorphism
↓
Allows substitution
LSP
↓
Makes sure substitution is behaviorally safe
24. LSP and Inheritance
Inheritance should represent more than a technical code-reuse relationship.
You should ask:
Is the child truly substitutable for the parent in the context of the parent's contract?
Not simply:
Can I reuse some code by inheriting this class?
Bad inheritance often leads to LSP violations.
Sometimes composition is better than inheritance.
25. Common Signs of an LSP Violation
Watch for child classes containing things like:
throw new NotSupportedException();
or:
throw new NotImplementedException();
for operations required by the base abstraction.
Also watch for:
if (obj is SomeSpecificChild)
{
// special handling
}
or:
if (obj.GetType() == typeof(SomeChild))
{
// don't execute normal behavior
}
These don't automatically prove an LSP violation, but they are useful warning signs.
26. Advantages of LSP
1. Reliable Polymorphism
You can safely work with abstractions:
Bird bird;
without constantly checking the actual concrete type.
2. Better Inheritance Design
LSP forces us to think carefully about whether inheritance relationships are logically correct.
3. Fewer Runtime Errors
We avoid situations such as:
Parent method exists
↓
Child cannot support it
↓
NotSupportedException
4. Easier Maintenance
Consumers don't need lots of special cases for particular derived classes.
5. Better Extensibility
New implementations can be introduced while preserving the expected contract.
6. Better Testing
Tests written around an abstraction can often be applied consistently across its implementations.
7. Cleaner Code
Instead of:
if Sparrow
Fly
if Penguin
Don't Fly
if Ostrich
Don't Fly
if Eagle
Fly
we model the capability properly:
IFlyingBird
↑
┌───┴───┐
Sparrow Eagle
27. LSP in ASP.NET Core
You may not explicitly say:
"I'm implementing LSP."
But you use the idea frequently.
For example:
IEmailService
↑
┌────────┴────────┐
│ │
SmtpEmailService SendGridEmailService
Your business service might depend on:
IEmailService
If both implementations honor the same contract, either should be usable without forcing the business service to change its logic.
Conceptually:
OrderService
│
↓
IEmailService
↑
│
SmtpEmailService
can potentially become:
OrderService
│
↓
IEmailService
↑
│
SendGridEmailService
without surprising OrderService.
That's LSP in practical application design.
28. LSP vs OCP
Since you just covered Open/Closed Principle, this distinction is useful.
OCP
Asks:
Can I add another implementation without repeatedly modifying stable processing code?
LSP
Asks:
Will that new implementation behave correctly wherever the abstraction is expected?
For example:
INotification
↑
┌─────────┼─────────┐
│ │ │
Email SMS WhatsApp
OCP:
Adding WhatsApp should require minimal modification to existing processing logic.
LSP:
WhatsApp must correctly satisfy the INotification contract.
So they work together:
OCP
↓
Easy to add implementation
LSP
↓
New implementation behaves correctly
29. Important Interview Points
Does inheritance automatically satisfy LSP?
No.
Parent obj = new Child();
may compile while the child's behavior still violates the parent's contract.
Is LSP only for abstract classes?
No.
The concept applies broadly to subtype relationships and abstractions, including:
- Base/derived classes
- Abstract classes
- Interface implementations
Is NotSupportedException always an LSP violation?
No.
It depends on the contract.
But if the parent/interface promises an operation and a subtype cannot support that operation, it is a strong indication that the abstraction may be incorrectly designed.
What is the main goal?
Safe behavioral substitution.
30. Key Points
- LSP = Liskov Substitution Principle.
- It is the L in SOLID.
- A derived type should be safely usable wherever its base type is expected.
- LSP is about behavior, not merely whether code compiles.
- A child should respect the contract established by its parent or interface.
- A child shouldn't unexpectedly reject valid operations promised by the parent.
- A child shouldn't provide weaker guarantees than the parent promises.
NotSupportedExceptionin a required inherited operation can be a warning sign.- LSP encourages better inheritance and abstraction design.
- Don't use inheritance only for code reuse.
- Sometimes splitting capabilities into smaller abstractions or using composition is a better design.
- LSP makes polymorphism reliable.
- OCP helps us add new implementations; LSP helps ensure those implementations can safely substitute for the abstraction.
Interview Answer
Liskov Substitution Principle states that a derived class should be able to replace its base class without breaking the application or violating the expected behavior. In simple terms, if the application works with a parent type, it should continue to work correctly when we provide any valid child type. A subtype must honor the behavioral contract of its parent. This makes inheritance and polymorphism safer, more predictable, and easier to maintain.