SOLID Principles Explained with Examples
The SOLID principles explained with code examples: single responsibility, open-closed, Liskov substitution, interface segregation and dependency inversion.
The SOLID principles are five guidelines for object-oriented design that make code easier to change, test and understand: Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion. They were collected by Robert C. Martin and are the vocabulary of low-level design interviews. Each one is a way of limiting how far a change has to spread.
S: Single responsibility principle
A class should have one reason to change. An Invoice class that calculates totals, formats a PDF and emails it changes whenever tax rules, PDF layout or email providers change.
// Before: one class, three reasons to change
class Invoice { total() {...} toPdf() {...} email() {...} }
// After: each class changes for one reason
class Invoice { total() {...} }
class InvoicePdfRenderer { render(invoice) {...} }
class InvoiceMailer { send(invoice, pdf) {...} }O: Open-closed principle
Open for extension, closed for modification. Adding a new payment method shouldn't require editing a growing if/else in the checkout code. Put the varying behaviour behind an interface and add new implementations.
interface PaymentMethod { pay(amount: Money): Receipt }
class CardPayment implements PaymentMethod { ... }
class UpiPayment implements PaymentMethod { ... }
class WalletPayment implements PaymentMethod { ... } // added without touching Checkout
class Checkout {
constructor(private method: PaymentMethod) {}
complete(amount: Money) { return this.method.pay(amount) }
}L: Liskov substitution principle
Subtypes must be usable wherever their base type is expected, without surprises. The classic violation is Square extends Rectangle: setting a square's width also changes its height, so code that sets width and height independently breaks. If a subclass has to throw "not supported" for a base-class method, or weakens what the base promised, the inheritance is wrong; prefer composition or a different abstraction.
- Subclasses shouldn't strengthen preconditions (accept less) or weaken postconditions (promise less).
- A
ReadOnlyListthat inherits fromListand throws onaddbreaks every caller expecting a list.
I: Interface segregation principle
Clients shouldn't depend on methods they don't use. A Machine interface with print, scan and fax forces a simple printer to implement fax. Split it into Printer, Scanner and Fax, and let a multifunction device implement all three.
D: Dependency inversion principle
High-level policy should depend on abstractions, not on low-level details. An OrderService that creates a MySqlOrderRepository directly is welded to MySQL and hard to test. Depend on an OrderRepository interface and inject the implementation.
interface OrderRepository { save(order: Order): void; findById(id: string): Order | null }
class OrderService {
constructor(private repo: OrderRepository, private events: EventPublisher) {}
place(order: Order) { this.repo.save(order); this.events.publish("order.placed", order.id) }
}
// production: new OrderService(new SqlOrderRepository(pool), new KafkaPublisher(...))
// tests: new OrderService(new InMemoryOrderRepository(), new FakePublisher())Using SOLID well
| Principle | Smell it fixes | Typical tool |
|---|---|---|
| Single responsibility | God classes; unrelated changes collide | Split classes by reason to change |
| Open-closed | Long type-switching conditionals | Interfaces plus polymorphism, strategy pattern |
| Liskov substitution | Subclasses that throw or behave surprisingly | Composition over inheritance |
| Interface segregation | Fat interfaces with empty implementations | Small, role-based interfaces |
| Dependency inversion | Hard-coded dependencies, untestable code | Interfaces and dependency injection |
SOLID is a means, not a goal. Applying every principle everywhere creates layers of interfaces with one implementation each. In an interview, apply them where requirements say behaviour will vary, and say so: "pricing rules change often, so I'll put them behind a strategy interface".
Where it’s used
Parking lots, elevators and payment systems are judged on clear responsibilities and extensible interfaces.
Dependency inversion lets business logic run in tests with in-memory fakes.
Open-closed design lets new integrations be added without modifying core code.
In an interview
- Define each principle in one sentence with an example.
- Show a before-and-after refactor for open-closed or dependency inversion.
- Spot Liskov violations in inheritance hierarchies.
- Apply principles where change is expected, not everywhere.
Further reading
- Solid RelevanceRobert C. Martin, The Clean Code Blog
- SOLIDWikipedia