SOLID Principles Explained with Examples

The SOLID principles explained with code examples: single responsibility, open-closed, Liskov substitution, interface segregation and dependency inversion.

Beginner 3 min read · Updated

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.

splitsplitsplitimplementsOrderManagerprices, saves, emailsPriceCalculatorOrderRepositoryNotifierinterfaceEmailNotifier
Single responsibility and dependency inversion together: one class that did everything becomes small parts, and orders depend on a notifier interface rather than on email.

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 ReadOnlyList that inherits from List and throws on add breaks 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

PrincipleSmell it fixesTypical tool
Single responsibilityGod classes; unrelated changes collideSplit classes by reason to change
Open-closedLong type-switching conditionalsInterfaces plus polymorphism, strategy pattern
Liskov substitutionSubclasses that throw or behave surprisinglyComposition over inheritance
Interface segregationFat interfaces with empty implementationsSmall, role-based interfaces
Dependency inversionHard-coded dependencies, untestable codeInterfaces 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

LLD interviews

Parking lots, elevators and payment systems are judged on clear responsibilities and extensible interfaces.

Testable services

Dependency inversion lets business logic run in tests with in-memory fakes.

Plugin architectures

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