Liskov Substitution Principle (LSP) in C# with E-Commerce Payment Example

What is the Liskov Substitution Principle (LSP)?

The Liskov Substitution Principle (LSP) is one of the five SOLID principles of object-oriented programming. It states that objects of a base class should be replaceable with objects of its derived classes without affecting the correctness of the program.

In simple terms, subclasses should follow the behavior of their base class, allowing any code that works with the base class to function correctly with its subclasses.

In this guide, we’ll walk through an e-commerce application where a payment system needs to be implemented and later extended with new providers. First, we’ll look at how the Liskov Substitution Principle (LSP) can be broken, and then demonstrate the correct implementation that follows LSP to make the codebase extensible, scalable, and easy to maintain.

Liskov Substitution Principle (LSP) in C# with real-world e-commerce payment example

Why LSP Matters in C# and SOLID Principles?

LSP is one of the most important for writing maintainable, scalable, and extensible C# code.

Without LSP:

  • Subclasses may break the behavior expected by the base class.
  • Code becomes difficult to test and maintain.
  • Integration with other SOLID principles, like Dependency Inversion (DIP), becomes harder.

Breaking LSP: Common Mistakes in C#

Take an e-commerce application payment system as an example:

public class Payment
{
    public virtual void Process(decimal amount)
    {
        Console.WriteLine($"Processing payment of {amount}");
    }
}

public class CashOnDelivery : Payment
{
    public override void Process(decimal amount)
    {
        throw new NotImplementedException("Cash on Delivery cannot be processed online!");
    }
}

Problem:

  • If we substitute Payment payment = new CashOnDelivery(); and call payment.Process(1000);, it throws an exception, violating LSP.
  • The subclass CashOnDelivery does not behave like a normal Payment.
LSP violation and correct implementation in C# with e-commerce payment example

In the above image code snippet, the Liskov Substitution Principle (LSP) was violated because a derived class inherited from the base class but did not provide proper logic, instead throwing an exception. This approach breaks LSP and leads to fragile code. On the right side, we demonstrated the correct implementation that adheres to LSP, showing how to build an extensible, scalable, and maintainable system by following this SOLID principle.

Proper Implementation of Liskov Substitution Principle (LSP) in C#

To follow LSP, define an abstraction and implement subclasses correctly:

public interface IPayment
{
    void Pay(decimal amount);
}

public class CreditCardPayment : IPayment
{
    public void Pay(decimal amount)
    {
        Console.WriteLine($"Paid {amount:C} using Credit Card.");
    }
}

public class PayPalPayment : IPayment
{
    public void Pay(decimal amount)
    {
        Console.WriteLine($"Paid {amount:C} using PayPal.");
    }
}

public class CashOnDelivery : IPayment
{
    public void Pay(decimal amount)
    {
        Console.WriteLine($"Order placed. Pay {amount:C} on delivery.");
    }
}

✅ Now, With this design, any class implementing the IPayment interface can be used interchangeably, ensuring that subclasses can replace IPayment without causing errors or breaking the functionality of the system.

Now, suppose we want to add a new payment provider, `UPIPayment`. Here’s how we can extend the payment functionality in C# while following the Liskov Substitution Principle (LSP)

E-Commerce Example: Applying LSP with Payment Methods

Let’s extend the example for a real-world e-commerce scenario with multiple payment methods:

public class UpiPayment : IPayment
{
    public void Pay(decimal amount)
    {
        Console.WriteLine($"Paid {amount:C} using UPI.");
    }
}
  1. CreditCardPayment, PayPalPayment, CashOnDelivery, and UpiPayment all follow the same interface.
  2. Checkout code can work with any payment type seamlessly, demonstrating LSP in action.

Integrating Dependency Injection (DI) for LSP Compliance

By using Dependency Injection (DI) in C#, we can choose the payment method dynamically at runtime without changing the existing checkout logic. This approach makes the system flexible, maintainable, and fully compliant with the Liskov Substitution Principle (LSP), allowing new payment types to be added easily.

public interface IPaymentFactory
{
    IPayment GetPaymentMethod(string methodName);
}

public class PaymentFactory : IPaymentFactory
{
    private readonly IServiceProvider _serviceProvider;
    private readonly Dictionary<string, Type> _paymentTypes;

    public PaymentFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
        _paymentTypes = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
        {
            { "creditcard", typeof(CreditCardPayment) },
            { "paypal", typeof(PayPalPayment) },
            { "cod", typeof(CashOnDelivery) },
            { "upi", typeof(UpiPayment) }
        };
    }

    public IPayment GetPaymentMethod(string methodName)
    {
        if (_paymentTypes.TryGetValue(methodName, out var type))
        {
            return (IPayment)_serviceProvider.GetService(type)!;
        }
        throw new ArgumentException($"Payment method '{methodName}' is not supported.");
    }
}

The Checkout class uses the payment factory to select and process the appropriate payment method, keeping the system flexible and LSP-compliant.

public class Checkout
{
    private readonly IPaymentFactory _paymentFactory;

    public Checkout(IPaymentFactory paymentFactory)
    {
        _paymentFactory = paymentFactory;
    }

    public void CompleteOrder(string methodName, decimal amount)
    {
        var paymentMethod = _paymentFactory.GetPaymentMethod(methodName);
        paymentMethod.Pay(amount);
    }
}

DI container registration:

var services = new ServiceCollection();
services.AddTransient<CreditCardPayment>();
services.AddTransient<PayPalPayment>();
services.AddTransient<CashOnDelivery>();
services.AddTransient<UpiPayment>();
services.AddSingleton<IPaymentFactory, PaymentFactory>();
services.AddTransient<Checkout>();
var serviceProvider = services.BuildServiceProvider();

Example of Selecting Payment Method at Checkout Time

var checkout = serviceProvider.GetRequiredService<Checkout>();
Console.WriteLine("Choose payment method: creditcard / paypal / cod / upi");
var choice = Console.ReadLine();
checkout.CompleteOrder(choice!, 1500);

Output based on user payment selection

User selects PayPal:
Processing payment via PayPal...
Paid ₹1,500.00 using PayPal.
User selects UPI
Processing payment via UPI...
Paid ₹1,500.00 using UPI.
User selects Cash on Delivery:
Processing payment via COD...
Order placed. Pay ₹1,500.00 on delivery.

Summary

In this article, we explored how violating LSP affects code and then walked through a step-by-step implementation of LSP the right way, making our applications more flexible, scalable, and maintainable.
The Liskov Substitution Principle (LSP) in C# ensures flexible, scalable, and maintainable code. By applying this SOLID principle, derived classes can replace their base classes without breaking functionality.
Our e-commerce payment example with Dependency Injection in C# shows how PayPal, UPI, COD, and Credit Card work seamlessly.

🔗 Explore More from My Articles

💡 If this content helped you, do me a small favor:
👉 Repost & share with your network so more developers can benefit.
👉 Comment below with your thoughts — I’d love to hear how you approach these topics.

Together, we can share knowledge, grow as developers, and build cleaner .NET codebases 💻✨