October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk6 min

SOLID Principles in C#: A Practical Guide with Real-World Examples and Design Patterns

A practical C# guide to SRP, OCP, LSP, ISP, and DIP—with examples that show when abstractions and design patterns solve real design problems.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

SOLID is a set of five object-oriented design principles for managing responsibilities, behavior, interfaces, and dependencies in C#. Applied thoughtfully, the principles can make likely changes easier to isolate and implementations easier to substitute. They do not require a particular architecture or a class for every method: an abstraction is worthwhile when it addresses a real change, client need, or dependency boundary.

What SOLID means in C#

The five principles are Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. They are related, but each helps you examine a different design question: what a type is responsible for, how behavior varies, what callers can expect, which operations a client needs, and which way dependencies point.

As an Amazon Associate I earn from qualifying purchases.

Use them as design checks, not automatic refactoring rules. A useful comparison asks whether expected changes are localized, whether callers and implementations have clear obligations, whether substitution and testing are practical, whether dependencies point toward stable policy, and whether the extra structure is justified.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Single Responsibility Principle: keep reasons to change coherent

SRP says a type should have one responsibility—often summarized as one reason to change. It does not prescribe one method per class or a minimum class size. The practical question is whether a type combines concerns that change for different reasons. Microsoft Learn connects this principle to separation of concerns in its .NET architectural principles.

Example: separate order calculation from persistence

Suppose an order service both calculates a total and writes an order to a database. If pricing rules and database storage change independently, the service has two unrelated reasons to change.

public sealed class OrderService
{
    public decimal CalculateTotal(Order order)
    {
        return order.Items.Sum(item => item.Price * item.Quantity);
    }

    public void Save(Order order)
    {
        // Write the order to storage.
    }
}

Keep the calculation in a pricing component and move storage behind a persistence component. An application service can coordinate them, while each focused type handles one coherent concern. The refactor localizes pricing changes away from storage changes; it also introduces another type and a boundary to maintain.

This split is useful when calculation and persistence have distinct rules, owners, or expected changes. For a tiny application where the operations always change together, the combined class may be simpler and clearer.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Open/Closed Principle: extend where variation is expected

OCP describes a stable policy as open to extension and closed to repeated modification. In practice, identify behavior likely to vary, then provide an extension point so adding a supported variation does not require rewriting the stable core. It does not mean every conditional is a design flaw: a short switch over a genuinely fixed set of cases may be the clearest solution.

Example: payment methods that are expected to grow

If an application is expected to support several payment providers, a strategy makes their shared operation explicit:

public interface IPaymentMethod
{
    Task PayAsync(decimal amount);
}

public sealed class CheckoutService
{
    private readonly IPaymentMethod _paymentMethod;

    public CheckoutService(IPaymentMethod paymentMethod)
    {
        _paymentMethod = paymentMethod;
    }

    public Task CompleteAsync(decimal total)
    {
        return _paymentMethod.PayAsync(total);
    }
}

Each provider can implement IPaymentMethod; checkout depends on the operation it needs rather than branching on provider names. Adding a provider then adds an implementation instead of another provider-specific branch in checkout. The trade-off is an interface and multiple types, so this design fits a real, expected family of behaviors—not a hypothetical future.

Strategy is an example of a pattern that can express this variation; it is not required by OCP. A factory may also help when selecting or constructing a strategy is itself meaningful policy. Adding a factory when construction is already simple only adds indirection.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Liskov Substitution Principle: preserve behavioral expectations

LSP says that a replacement implementation must preserve the behavior its callers are entitled to rely on. A class can satisfy a C# interface or inherit from a base class and still violate its contract by rejecting valid inputs, providing weaker guarantees, or surprising a caller. The relevant question is not only whether it compiles, but whether it behaves as the caller expects.

Example: a read-only account must not pretend it can accept deposits

Suppose a base type promises that depositing a positive amount succeeds and increases the balance. A subtype that throws for every deposit is not a safe substitute:

public class Account
{
    public decimal Balance { get; protected set; }

    public virtual void Deposit(decimal amount)
    {
        if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount));
        Balance += amount;
    }
}

public sealed class ReadOnlyAccount : Account
{
    public override void Deposit(decimal amount)
    {
        throw new InvalidOperationException("Deposits are not supported.");
    }
}

A caller using an Account may validly call Deposit, but the subtype breaks that expectation. If some accounts are read-only, model the capabilities separately—for example, expose a read-only account view to readers and a deposit-capable contract only where deposits are supported. That makes caller obligations explicit, at the cost of additional types or interfaces.

Use inheritance when the subtype can honor the base contract. If it cannot, composition or narrower capabilities may fit better. Microsoft’s archived C# discussion of SOLID violations is an additional example-oriented reference.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Interface Segregation Principle: give clients the operations they need

ISP favors interfaces shaped around their clients, so a consumer need not depend on irrelevant operations and an implementation need not provide meaningless behavior.

Example: split a broad worker interface by capability

Imagine a broad interface with Print, Scan, and Fax. A printing client should not need a scanner or fax implementation just to print. Instead, define focused capabilities:

public interface IPrinter
{
    void Print(string document);
}

public interface IScanner
{
    byte[] Scan();
}

public sealed class PrintJob
{
    private readonly IPrinter _printer;

    public PrintJob(IPrinter printer)
    {
        _printer = printer;
    }

    public void Run(string document) => _printer.Print(document);
}

A basic printer can implement only IPrinter; a multifunction device can implement several capabilities. The client depends only on printing, and implementations need not fake unsupported operations. This adds contracts, so avoid creating tiny interfaces that have no distinct client or change boundary: excessive fragmentation can make the design harder to follow.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Dependency Inversion Principle: point policy toward abstractions

DIP says higher-level policy should not depend directly on lower-level implementation details; both should depend on abstractions. Microsoft Learn explains that this can invert compile-time dependencies while leaving runtime call flow intact. In a layered .NET application, for example, an application service can depend on a persistence contract while infrastructure supplies the database implementation. Microsoft’s overview of common web application architectures discusses logical separation into layers and the role of dependency inversion in modularity and testability.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Example: avoid constructing a database client inside policy code

When a high-level service constructs a concrete database client itself, its policy is coupled to the low-level library and is harder to substitute. Put the needed operation behind a contract:

public interface IOrderStore
{
    Task SaveAsync(Order order);
}

public sealed class OrderApplicationService
{
    private readonly IOrderStore _store;

    public OrderApplicationService(IOrderStore store)
    {
        _store = store;
    }

    public Task PlaceAsync(Order order)
    {
        // Apply application-level rules here.
        return _store.SaveAsync(order);
    }
}

Infrastructure can implement IOrderStore using a database, while tests can supply a substitute. The compile-time dependency now points from the application service to a contract instead of directly to a database client. The runtime call still flows from the service to its store implementation.

Dependency inversion is a principle; dependency injection is a technique for providing collaborators. Microsoft Learn puts it this way: “The practice of dependency injection is made possible by following the dependency inversion principle.” A .NET DI container can register and supply implementations, but using a container does not by itself establish DIP. Nor does every interface help: add one when it separates policy from a detail, enables a needed substitution, or establishes a useful boundary.

Patterns that can help at a boundary

  • Adapter: wrap an external API behind an application-facing contract so vendor-specific details stay at the boundary.
  • Decorator: wrap an implementation to add a cross-cutting behavior, such as logging, without modifying the wrapped type.
  • Factory: centralize creation or selection when that policy is non-trivial and expected to change.

These patterns can support dependency inversion, testing, or localized change, but none is mandated by DIP. Each adds types and indirection; use it when the boundary solves a concrete problem.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How the principles fit together in a real design

Consider an order-placement flow. A focused application service coordinates the use case (SRP); a payment strategy handles genuinely variable payment behavior (OCP); each implementation honors the payment contract callers rely on (LSP); clients depend only on capabilities they use (ISP); and the application service depends on contracts rather than database or provider details (DIP). The design need not have a particular number of layers, use repositories, or adopt Clean Architecture. Microsoft’s architecture guidance describes layering as a useful option for non-trivial business applications, not a universal mandate.

For a small, stable feature, direct code may be easier to understand. Add structure when there is actual change pressure, a meaningful client boundary, a behavioral contract to preserve, or a dependency worth isolating. More abstractions do not automatically mean a more maintainable design.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.