SOLID Principales
This project demonstrates how to apply the SOLID principles in a simple Payment Processing System using C#.
The goal is to show how following SOLID makes code cleaner, maintainable, and extensible.
The system processes orders using different payment methods and generates invoices.
It applies all five SOLID principles:
-
Single Responsibility Principle (SRP)
Each class has one responsibility.OrderRepositoryβ Saves ordersPdfInvoice/EmailInvoiceβ Generate invoicesCheckoutServiceβ Manages checkout process
-
Open/Closed Principle (OCP)
The system is open for extension but closed for modification.- New payment methods (e.g.,
CreditCardPayment,PayPalPayment,BitcoinPayment) can be added without changing existing code.
- New payment methods (e.g.,
-
Liskov Substitution Principle (LSP)
Subclasses can replace their parent classes without breaking behavior.- Any
IPaymentProcessor(Credit Card, PayPal, Bitcoin) can be used inCheckoutService.
- Any
-
Interface Segregation Principle (ISP)
Clients are not forced to implement methods they donβt use.IInvoiceGeneratoris separate fromIPaymentProcessor.
-
Dependency Inversion Principle (DIP)
High-level modules depend on abstractions, not concrete implementations.CheckoutServicedepends onIPaymentProcessorandIInvoiceGeneratorinterfaces.
// Each class has only one reason to change.
public class Order
{
public string OrderId { get; set; }
public decimal Amount { get; set; }
}
public class OrderRepository
{
public void Save(Order order)
{
Console.WriteLine($"Order {order.OrderId} saved to database.");
}
}// Add new payment methods without modifying existing logic.
public interface IPaymentProcessor
{
void ProcessPayment(Order order);
}
public class CreditCardPayment : IPaymentProcessor
{
public void ProcessPayment(Order order)
{
Console.WriteLine($"Processing Credit Card payment for {order.Amount}");
}
}
public class PayPalPayment : IPaymentProcessor
{
public void ProcessPayment(Order order)
{
Console.WriteLine($"Processing PayPal payment for {order.Amount}");
}
}// Subtypes (payment methods) can replace the parent type (IPaymentProcessor).
public class BitcoinPayment : IPaymentProcessor
{
public void ProcessPayment(Order order)
{
Console.WriteLine($"Processing Bitcoin payment for {order.Amount}");
}
}// Instead of one big interface, we separate responsibilities.
public interface IInvoiceGenerator
{
void GenerateInvoice(Order order);
}
public class PdfInvoice : IInvoiceGenerator
{
public void GenerateInvoice(Order order)
{
Console.WriteLine($"PDF invoice generated for order {order.OrderId}");
}
}
public class EmailInvoice : IInvoiceGenerator
{
public void GenerateInvoice(Order order)
{
Console.WriteLine($"Invoice emailed for order {order.OrderId}");
}
}// High-level modules depend on abstractions, not concretions.
public class CheckoutService
{
private readonly IPaymentProcessor _paymentProcessor;
private readonly IInvoiceGenerator _invoiceGenerator;
private readonly OrderRepository _orderRepository;
public CheckoutService(IPaymentProcessor paymentProcessor, IInvoiceGenerator invoiceGenerator, OrderRepository orderRepository)
{
_paymentProcessor = paymentProcessor;
_invoiceGenerator = invoiceGenerator;
_orderRepository = orderRepository;
}
public void Checkout(Order order)
{
_paymentProcessor.ProcessPayment(order);
_invoiceGenerator.GenerateInvoice(order);
_orderRepository.Save(order);
}
}Order order = new Order { OrderId = "123", Amount = 100.50m };
// Choose payment and invoice types
IPaymentProcessor payment = new PayPalPayment();
IInvoiceGenerator invoice = new PdfInvoice();
// Inject dependencies into CheckoutService
CheckoutService checkout = new CheckoutService(payment, invoice, new OrderRepository());
// Process checkout
checkout.Checkout(order);Processing PayPal payment for 100.50
PDF invoice generated for order 123
Order 123 saved to database.-
SRP β OrderRepository only saves orders, CheckoutService only manages checkout, Invoice classes only handle invoices.
-
OCP β We can add new payments (e.g., StripePayment) without changing existing classes.
-
LSP β Any payment type (CreditCard, PayPal, Bitcoin) can replace IPaymentProcessor without breaking behavior.
-
ISP β Invoices are split (PdfInvoice, EmailInvoice), not forced into one big interface.
-
DIP β CheckoutService depends on abstractions (IPaymentProcessor, IInvoiceGenerator) instead of concrete classes.
This way, the system is flexible, testable, and extendable.
public class PaymentProcessor
{
public void ProcessPayment(Order order, string paymentType)
{
if (paymentType == "CreditCard")
{
Console.WriteLine($"Processing Credit Card payment for {order.Amount}");
}
else if (paymentType == "PayPal")
{
Console.WriteLine($"Processing PayPal payment for {order.Amount}");
}
else if (paymentType == "Bitcoin")
{
Console.WriteLine($"Processing Bitcoin payment for {order.Amount}");
}
// β Every time we add a new payment type (Stripe, ApplePay),
// we must MODIFY this class β violates OCP
}
}Problem:
- Adding StripePayment means we must edit PaymentProcessor.
- The class keeps growing and becomes harder to maintain.
High chance of introducing bugs while modifying.
public interface IPaymentProcessor
{
void ProcessPayment(Order order);
}Now, imagine we create a GiftCardPayment class but force it to implement ProcessPayment, even though gift cards donβt work like normal payments:
public class GiftCardPayment : IPaymentProcessor
{
public void ProcessPayment(Order order)
{
// β GiftCard canβt really process payments like CreditCard/PayPal
// So we throw an exception
throw new NotSupportedException("GiftCard cannot process payment directly.");
}
}
public void Checkout(Order order)
{
_paymentProcessor.ProcessPayment(order); // β Will crash if GiftCardPayment is used
}Why this breaks LSP?
- LSP rule: Subclasses (or implementations) must be usable anywhere the base type is expected.
- Here, if we substitute GiftCardPayment for IPaymentProcessor, it throws an exception instead of behaving properly.
- Client code (CheckoutService) now has to know special cases β violates LSP.
β Fixing LSP
Instead of forcing GiftCardPayment into the wrong interface, we restructure abstractions:
public interface IPaymentProcessor
{
void ProcessPayment(Order order);
}
public interface IGiftCardRedemption
{
void RedeemGiftCard(Order order);
}
public class CreditCardPayment : IPaymentProcessor
{
public void ProcessPayment(Order order)
{
Console.WriteLine($"Processing Credit Card payment for {order.Amount}");
}
}
public class PayPalPayment : IPaymentProcessor
{
public void ProcessPayment(Order order)
{
Console.WriteLine($"Processing PayPal payment for {order.Amount}");
}
}
public class GiftCardRedemption : IGiftCardRedemption
{
public void RedeemGiftCard(Order order)
{
Console.WriteLine($"Redeeming gift card for {order.Amount}");
}
}β Why this respects LSP?
- Each implementation behaves correctly without throwing exceptions.
- GiftCardRedemption is no longer pretending to be a normal payment processor.
- Substitution works: anywhere you expect IPaymentProcessor, you can safely use CreditCardPayment or PayPalPayment without breaking behavior.
Suppose we design one fat interface that tries to handle every possible invoice format:
public interface IInvoiceGenerator
{
void GeneratePdfInvoice(Order order);
void GenerateEmailInvoice(Order order);
void GenerateExcelInvoice(Order order);
}Now, classes are forced to implement methods they donβt need:
public class PdfInvoiceGenerator : IInvoiceGenerator
{
public void GeneratePdfInvoice(Order order)
{
Console.WriteLine($"PDF invoice generated for order {order.OrderId}");
}
public void GenerateEmailInvoice(Order order)
{
// β Not applicable β forced to implement
throw new NotImplementedException();
}
public void GenerateExcelInvoice(Order order)
{
// β Not applicable β forced to implement
throw new NotImplementedException();
}
}π¨ Why this breaks ISP?
-
Classes should not be forced to depend on methods they donβt use.
-
Here, PdfInvoiceGenerator only cares about PDF, but is forced to implement email and Excel too.
-
Any change in IInvoiceGenerator impacts all implementations unnecessarily.
-
β Fixing ISP
Split the interface into smaller, more specific interfaces:
public interface IPdfInvoiceGenerator
{
void GeneratePdfInvoice(Order order);
}
public interface IEmailInvoiceGenerator
{
void GenerateEmailInvoice(Order order);
}
public interface IExcelInvoiceGenerator
{
void GenerateExcelInvoice(Order order);
}Now, each implementation only does what it should:
public class PdfInvoiceGenerator : IPdfInvoiceGenerator
{
public void GeneratePdfInvoice(Order order)
{
Console.WriteLine($"PDF invoice generated for order {order.OrderId}");
}
}
public class EmailInvoiceGenerator : IEmailInvoiceGenerator
{
public void GenerateEmailInvoice(Order order)
{
Console.WriteLine($"Invoice emailed for order {order.OrderId}");
}
}β Why this respects ISP?
- Each class depends only on what it really needs.
- No unnecessary throw new NotImplementedException().
- Easier to maintain and extend.
CheckoutService does depend on abstractions (IPaymentProcessor, IInvoiceGenerator), which is good (follows DIP). If it did not depend on abstractions, it would look like this (bad example β):
public class CheckoutService
{
private readonly CreditCardPayment _creditCardPayment;
private readonly PdfInvoice _pdfInvoice;
private readonly OrderRepository _orderRepository;
public CheckoutService()
{
// Directly instantiating concrete classes (bad practice)
_creditCardPayment = new CreditCardPayment();
_pdfInvoice = new PdfInvoice();
_orderRepository = new OrderRepository();
}
public void Checkout(Order order)
{
_creditCardPayment.ProcessPayment(order);
_pdfInvoice.GenerateInvoice(order);
_orderRepository.Save(order);
}
}π¨ Whatβs wrong here?
- Tight coupling β CheckoutService is locked to CreditCardPayment and PdfInvoice.
- No flexibility β If we want to switch to PayPalPayment or EmailInvoice, we must modify CheckoutService.
- Hard to test β Canβt mock dependencies easily in unit tests.
β Why this version is better:
- Depends on interfaces (abstractions).
- Easy to swap different implementations without modifying CheckoutService.
- Easy to test with mocks.
public CheckoutService(IPaymentProcessor paymentProcessor, IInvoiceGenerator invoiceGenerator, OrderRepository orderRepository)So the difference is:
- Without DIP β CheckoutService creates and depends on concrete classes.
- With DIP β CheckoutService depends on interfaces/abstractions provided from outside (injected).