
SOLID Principles in C#: A Practical Guide with Examples and Quiz
Learn all five SOLID principles with practical C# examples, common design pitfalls, revision flashcards, and interview-style quizzes.
SOLID is a set of five object-oriented design principles that help make code easier to change, test, and extend. They are guidelines rather than rigid laws: apply them when they make a design clearer, not just to add more interfaces or classes.
Best practice:Use green for correct or preferred design guidance.Pitfall:Use red for a pitfall that couples unrelated behavior or breaks a contract.Caution:Use orange for a caution: avoid adding abstractions without a real need.At a glance
| Letter | Principle | Main idea |
|---|---|---|
| S | Single Responsibility Principle | A module has one reason to change. |
| O | Open/Closed Principle | Extend behavior without repeatedly rewriting stable code. |
| L | Liskov Substitution Principle | A subtype must honor the expectations of its base type. |
| I | Interface Segregation Principle | Clients should not depend on methods they do not use. |
| D | Dependency Inversion Principle | High-level policy depends on abstractions, not low-level details. |
1. Single Responsibility Principle (SRP)
SRP says a class or module should have one reason to change. Think in terms of the stakeholders or concerns that cause change, rather than counting methods.
For example, a class that calculates an invoice total and also sends it by email mixes pricing policy with delivery. A change to email infrastructure should not require editing or retesting the pricing rules.
public sealed class InvoiceCalculator
{
public decimal CalculateTotal(IEnumerable<decimal> lineTotals) =>
lineTotals.Sum();
}
public sealed class InvoiceEmailSender(IEmailClient emailClient)
{
public Task SendAsync(Invoice invoice, CancellationToken cancellationToken) =>
emailClient.SendAsync(invoice.CustomerEmail, invoice.ToEmailBody(), cancellationToken);
}Best practice:Keep each type focused on a coherent policy or reason to change.Quick check: What is the best description of SRP?
2. Open/Closed Principle (OCP)
OCP says software should be open to extension but closed to modification. When new variations are likely, polymorphism can let you add behavior without expanding a central conditional every time.
public interface IDiscountPolicy
{
decimal Apply(decimal subtotal);
}
public sealed class NoDiscount : IDiscountPolicy
{
public decimal Apply(decimal subtotal) => subtotal;
}
public sealed class PercentageDiscount(decimal rate) : IDiscountPolicy
{
public decimal Apply(decimal subtotal) => subtotal * (1 - rate);
}
public sealed class CheckoutService(IDiscountPolicy discountPolicy)
{
public decimal Total(decimal subtotal) => discountPolicy.Apply(subtotal);
}CheckoutService does not need a new if branch for each discount. A new policy can be added behind the same contract.
Quick check: How does CheckoutService support a new discount policy?
3. Liskov Substitution Principle (LSP)
LSP says a value of a subtype should be usable anywhere its base type is expected without breaking the program's promised behavior. In practice, derived types must respect the base contract: they should not unexpectedly reject valid inputs, weaken guarantees, or replace expected results with unrelated failures.
If a Bird abstraction promises that every bird can fly, a flightless bird cannot safely fulfill that contract. Model the capability separately instead:
public abstract class Bird { }
public interface IFlyingBird
{
void Fly();
}
public sealed class Eagle : Bird, IFlyingBird
{
public void Fly() { /* flight behavior */ }
}
public sealed class Penguin : Bird
{
public void Swim() { /* swimming behavior */ }
}Pitfall:A subtype that throws NotSupportedException for a base operation callers are entitled to use is a warning that the base contract is too broad.Quick check: Why is a flightless subtype problematic when Bird promises Fly()?
4. Interface Segregation Principle (ISP)
ISP says clients should not be forced to depend on methods they do not use. Instead of one large interface for every kind of worker, separate capabilities so each implementation only takes on relevant behavior.
public interface IPrinter
{
void Print(Document document);
}
public interface IScanner
{
ScanResult Scan(Document document);
}
public sealed class BasicPrinter : IPrinter
{
public void Print(Document document) { /* print document */ }
}BasicPrinter no longer needs to implement a fake Scan method or throw because scanning is unsupported.
Quick check: What is the main purpose of ISP?
5. Dependency Inversion Principle (DIP)
DIP says high-level policy should not depend directly on low-level details; both should depend on abstractions. Details such as an HTTP client, database, or file system can then be replaced without embedding them into the business rule.
public interface IOrderRepository
{
Task SaveAsync(Order order, CancellationToken cancellationToken);
}
public sealed class PlaceOrder(IOrderRepository repository)
{
public Task ExecuteAsync(Order order, CancellationToken cancellationToken) =>
repository.SaveAsync(order, cancellationToken);
}PlaceOrder depends on the repository contract. A production database adapter and an in-memory test adapter can both implement that contract.
Quick check: Which statement best distinguishes DIP from dependency injection?
Quick revision flashcards
Review a prompt, reveal its answer, and mark it known. Your progress is saved in this browser.
A practical SOLID checklist
Before adding an abstraction or splitting a class, ask:
- Which stakeholder or policy would cause this code to change?
- Is a frequently changing conditional growing in a stable workflow?
- Can each subtype honor the promises made by its base type?
- Do all interface clients need every operation?
- Does business policy know about infrastructure details it could instead depend on through a contract?
Interview-ready summary
SOLID stands for Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. Together, the principles encourage focused responsibilities, deliberate extension points, substitutable contracts, client-sized interfaces, and business rules that are not coupled to infrastructure details.