← Back to Article List         
Liskov Substitution Principle (LSP)

Liskov Substitution Principle (LSP)

Published on 28 Sep 2026     11 min read SOLID Principles
SOLID Principles

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 call Fly().

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.
  • NotSupportedException in 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.