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.
On this page · 6 min · Intermediate
On this page
- In simple words
- Learning objectives
- Explanation
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 27, 2026
Related tutorials
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#
// ✗ 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>();
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#
- The “before” class calculates, formats and sends: three reasons to change (SRP violation).
- It also creates SmtpClient itself, so it cannot be tested without a real mail server (DIP violation).
- The refactor extracts each responsibility behind a small interface (ISP).
- InvoiceService now depends only on abstractions supplied through its constructor (DIP).
- 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#
Splitting a 20-line class into five interfaces “for SOLID” makes code harder to read. Apply the principles where change is likely.
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?
Show solution
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#
Which principle says a class should have only one reason to change?
Want more? Practise all OOP Concepts MCQs · OOP Concepts interview questions
My notes#
Saved in this browser.