OOP Concepts · Design Principles

SOLID Principles

SOLID is five design principles that keep object-oriented code easy to change. They come up in almost every mid-level and senior developer interview.

Intermediate 6 min Lesson 22 of 38Examples use modern JavaScript (ES2022 private fields) and C# 13.
Before you start:Abstraction
0/38 in OOP Concepts
On this page · 6 min · Intermediate

In simple words#

SOLID is five rules of good carpentry for code: each tool one job, extend without breaking, replaceable parts, small specific tools, and depend on standards not brands.

S · O · L
Single responsibility · Open/closed · Liskov substitution.
I · D
Interface segregation · Dependency inversion.
Goal
Code that is easy to change safely.

Learning objectives#

  • Name and explain all five SOLID principles
  • Spot violations in real code
  • Refactor a class to follow SRP and DIP
  • Explain how SOLID relates to dependency injection and testing

Explanation#

S — Single Responsibility: a class should have one reason to change. An InvoiceService that also formats PDFs and sends emails has three.

O — Open/Closed: code should be open for extension but closed for modification. Add new behaviour by adding classes (polymorphism), not by editing a growing switch statement.

L — Liskov Substitution: a subclass must be usable anywhere its base class is expected, without surprises. If Square breaks code written for Rectangle, the hierarchy is wrong.

I — Interface Segregation: prefer several small interfaces over one large one, so classes are not forced to implement methods they do not use.

D — Dependency Inversion: high-level code should depend on abstractions, not on concrete low-level classes. Dependency injection is the usual way to achieve it.

Code example: Refactoring towards SRP and DIP#

C#InvoiceService.cs Illustrative
// ✗ Before: one class, three reasons to change, hard-wired dependencies
public class InvoiceServiceBefore
{
    public void Process(Invoice invoice)
    {
        var total = invoice.Lines.Sum(l => l.Price * l.Qty);          // pricing rules
        var pdf = 
quot;INVOICE
{invoice.Id}: {total}"; // formatting
new SmtpClient("smtp.example.com").Send("billing@x.com", // delivery invoice.CustomerEmail, "Invoice", pdf); } } // ✓ After: each responsibility behind its own abstraction public interface IInvoiceCalculator { decimal Total(Invoice invoice); } public interface IInvoiceFormatter { string Format(Invoice invoice, decimal total); } public interface IEmailSender { Task SendAsync(string to, string subject, string body); } public class InvoiceService(IInvoiceCalculator calc, IInvoiceFormatter formatter, IEmailSender email) { public async Task ProcessAsync(Invoice invoice) { var total = calc.Total(invoice); var body = formatter.Format(invoice, total); await email.SendAsync(invoice.CustomerEmail,
quot;Invoice
{invoice.Id}", body);
} } // Program.cs — wire up concrete implementations once // builder.Services.AddScoped<IInvoiceCalculator, StandardCalculator>(); // builder.Services.AddScoped<IInvoiceFormatter, PdfFormatter>(); // builder.Services.AddScoped<IEmailSender, SmtpEmailSender>(); // builder.Services.AddScoped<InvoiceService>();
Expected output
Behaviour is unchanged, but pricing, formatting and email delivery can now each change — or be faked in a unit test — independently.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. The “before” class calculates, formats and sends: three reasons to change (SRP violation).
  2. It also creates SmtpClient itself, so it cannot be tested without a real mail server (DIP violation).
  3. The refactor extracts each responsibility behind a small interface (ISP).
  4. InvoiceService now depends only on abstractions supplied through its constructor (DIP).
  5. Adding a GST-aware calculator means writing a new IInvoiceCalculator class — no edits to InvoiceService (OCP).

Real-world use case#

Unit testing InvoiceService with fake IEmailSender and IInvoiceCalculator implementations becomes trivial — no SMTP server, no database.

Common mistakes#

Treating SOLID as rigid law

Splitting a 20-line class into five interfaces “for SOLID” makes code harder to read. Apply the principles where change is likely.

The Square/Rectangle trap

If setting Width on a Square also changes Height, code that expects a Rectangle breaks. That is a Liskov Substitution violation.

Best practices#

  • Ask “how many reasons does this class have to change?”
  • Inject dependencies through constructors
  • Keep interfaces small and role-based
  • Use SOLID as a guide when code becomes hard to change, not as up-front ceremony

Remember this#

S = one reason to change. O = extend, don’t edit.

L = children keep parents’ promises. I = small interfaces.

D = depend on abstractions, not on details.

Practice exercise#

Your task: Identify which SOLID principle each smell violates: (1) a PaymentProcessor with a switch on payment type that grows every month; (2) an IWorker interface with Work() and Eat() that robots must implement; (3) a ReportService that creates new SqlConnection() inside its methods.

Need a hint?
Ask for each: is it a growing switch (O), a forced method (I), or a hard-coded dependency (D)?
Show solution
Text Illustrative
1. Open/Closed — add new payment types as classes implementing IPayment instead of editing the switch.
2. Interface Segregation — split into IWorkable and IFeedable.
3. Dependency Inversion — depend on an IReportRepository injected through the constructor.

Mini quiz#

SOLID Principles quizMultiple choice Question 1 of 3

Which principle says a class should have only one reason to change?

Score so far: 0/0

Want more? Practise all OOP Concepts MCQs · OOP Concepts interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.