All Blogs Best Practices 4 min read

SOLID Principles Explained with Real C# Examples

Sandeep Pal
June 3, 2026
SOLID Principles Explained with Real C# Examples

Why we still teach SOLID in 2025

SOLID helps you change software without breaking unrelated parts. Interviewers ask about it; senior developers expect it in pull requests. The mistake is treating SOLID as five bullet points to memorize. We use real C# examples from line-of-business apps— invoicing, notifications, pricing—so you can spot violations in your own codebase today.

S — Single Responsibility Principle

A class should have one reason to change. If your OrderService calculates totals, sends emails, and writes audit logs, three different stakeholders will fight over that file.

Before

public class OrderService
{
    public void PlaceOrder(Order order)
    {
        _db.Orders.Add(order);
        _db.SaveChanges();
        var smtp = new SmtpClient();
        smtp.Send(/* ... */);
        File.AppendAllText("audit.log", order.Id.ToString());
    }
}

After

Split persistence, notification, and auditing into separate types injected into a thin orchestrator. Each class changes when its own dependency changes—SQL schema vs email template vs audit policy.

public class OrderService
{
    private readonly IOrderRepository _repo;
    private readonly IOrderNotifier _notifier;
    private readonly IAuditWriter _audit;

    public async Task PlaceOrderAsync(Order order, CancellationToken ct)
    {
        await _repo.AddAsync(order, ct);
        await _notifier.SendConfirmationAsync(order, ct);
        await _audit.WriteAsync($"Order {'{'}order.Id{'}'} placed", ct);
    }
}

O — Open/Closed Principle

Open for extension, closed for modification. Adding a new discount type should not require editing a giant switch in your pricing engine.

public interface IDiscountRule
{
    bool AppliesTo(Order order);
    decimal Apply(decimal subtotal);
}

public class VolumeDiscountRule : IDiscountRule { /* ... */ }
public class LoyaltyDiscountRule : IDiscountRule { /* ... */ }

public class PricingCalculator
{
    private readonly IEnumerable<IDiscountRule> _rules;
    public decimal Total(Order order) =>
        _rules.Where(r => r.AppliesTo(order))
              .Aggregate(order.Subtotal, (total, rule) => rule.Apply(total));
}

Register new rules in DI; PricingCalculator stays untouched.

L — Liskov Substitution Principle

Subtypes must be substitutable for their base types without breaking callers. Classic violation: a Square that inherits Rectangle and throws when width ≠ height.

In application code, watch interfaces like IReadRepository that secretly throw on Update in a read-only implementation. Callers expecting repository behavior get surprises. Prefer small, focused interfaces (IReadOnlyOrderQueries) over fat ones half-implemented.

I — Interface Segregation Principle

Clients should not depend on methods they do not use. A fat IEmployeeService with payroll, scheduling, and performance reviews forces mock hell in tests.

  • Split into IEmployeeQueries, IPayrollProcessor, IScheduleManager.
  • Controllers depend only on the narrow interface they need.
  • API surface stays clearer for implementers.

D — Dependency Inversion Principle

High-level modules depend on abstractions, not concrete SQL or SMTP details. ASP.NET Core DI makes this practical:

builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();
builder.Services.AddScoped<IOrderNotifier, EmailOrderNotifier>();

Domain and application layers reference interfaces they own; infrastructure implements them. Tests swap fakes without a database.

Applying SOLID without over-engineering

Not every class needs seven interfaces. Apply SOLID when you feel pain: frequent merge conflicts in one god class, switches that grow every sprint, or tests that need a full SQL Server instance to run.

  1. Identify the change you expect next quarter.
  2. Introduce an abstraction at that seam only.
  3. Refactor incrementally— strangler fig, not big bang.

Code review prompts we use

  • Does this class name match everything inside it?
  • Will a new variant require editing existing methods or adding a new class?
  • Can I mock this dependency in a unit test in under ten lines?
  • Does this interface method belong on every implementation?

Connection to patterns you already know

Strategy maps to Open/Closed. Adapter helps invert dependencies to third-party SDKs. Decorator adds cross-cutting behavior without modifying core services. SOLID is the "why"; patterns are often the "how."

Next time you open a pull request, pick one principle and ask whether the diff makes change easier or harder six months from now. That habit matters more than reciting definitions in an interview—and interviewers notice when you tie principles to code on the screen.

1 views 0 likes 0 comments
Comments (0)
Sign in to leave a comment
Toolliyo Assistant
Ask about tutorials, ebooks, training, pricing, mentor services, and support. I use public site content only—not admin or internal tools.

care@toolliyo.com

Need callback? Share your details