← System Design
🧱
System Design

SOLID Principles

Five rules that keep code flexible: Single Responsibility, Open/Closed, Liskov, Interface Segregation, Dependency Inversion.

TL;DR

SOLID is five design principles that prevent code from rotting as it grows. Single Responsibility — one class, one reason to change. Open/Closed — open to extension, closed to modification. Liskov — subtypes must honor the contract. Interface Segregation — many small interfaces beat one fat one. Dependency Inversion — depend on abstractions, not concretions. Together they let teams change parts of a system without rebuilding the whole thing.

When to use

Every time you're building anything that will live longer than a sprint and be touched by more than one person. SOLID is not about perfection — it's about keeping coupling low enough that tomorrow's change doesn't require rewriting today's code. Critically important in service-oriented architectures where modules become deployable units.

Try it

Why SOLID matters for system design

System design interviews are about trade-offs at the architecture level — but the building blocks are still classes and modules. SOLID is the bridge.

The five, with one-line memory hooks

  1. S — Single Responsibility · A class should have one reason to change. If you describe what it does using “and”, split it.
  2. O — Open/Closed · You should be able to add new behavior without modifying existing code. Strategy and Decorator patterns exist for this.
  3. L — Liskov Substitution · Anywhere a parent type is expected, any subtype should work without surprising the caller. If overriding requires throwing, your hierarchy is broken.
  4. I — Interface Segregation · Don’t force a client to depend on methods it doesn’t use. Many narrow interfaces beat one wide one.
  5. D — Dependency Inversion · High-level policy shouldn’t depend on low-level details — both should depend on an abstraction.

The connection to system design

SOLID principleSystem-level expression
SRPMicroservices owning one bounded context
OCPPlugin architectures, gateways with pluggable middleware
LSPAPI versioning that doesn’t break existing clients
ISPAPI gateways exposing role-specific endpoints (BFF pattern)
DIPDependency on message brokers / interfaces, not concrete services

Trade-offs

📺 Video

A YouTube walkthrough — coming once the deep-dive video is published.

🧪 Simulator

An interactive simulator for this concept is on the way — tweak the knobs, watch behaviour change in real time.

💻 Code

A 30-line build challenge with starter code, hints, and a reference implementation.

🎯 Common interview questions
Q1. Give a real example of violating Single Responsibility.

A `User` class that contains `save()` (database I/O), `sendWelcomeEmail()` (network I/O), and `validatePassword()` (business logic). Three reasons to change — schema migration, email provider swap, password policy change — each forces you to touch and re-test the same class. Split it into `User`, `UserRepository`, `UserNotifier`, `PasswordPolicy`.

Q2. How does Open/Closed actually work in practice?

You design an extension point — usually an interface or a strategy slot — so new behavior plugs in without editing existing code. Example — a payment processor that accepts a `PaymentMethod` interface. Adding Apple Pay means writing a new `ApplePayMethod` class; the processor itself stays untouched (and the existing tests stay green).

Q3. What's the most concrete way to spot a Liskov violation?

A subtype that throws `NotSupportedException` for an inherited method. The classic textbook case is `Square extends Rectangle` — setting width on a Square has to also change height, breaking what every Rectangle caller expects. The subtype isn't substitutable, so the inheritance is wrong.

Q4. Why is Interface Segregation a big deal in microservices?

A bloated interface forces every consumer to depend on every method, so any signature change becomes a coordinated deployment across teams. Splitting `IUserService` into `IUserReader`, `IUserWriter`, `IUserAuth` lets each consumer depend only on what it actually uses, drastically reducing the blast radius of changes.

Q5. Dependency Inversion vs Dependency Injection — same thing?

No. Dependency Inversion is the *principle* — high-level modules depend on abstractions. Dependency Injection is one *technique* to achieve it — the framework hands a concrete implementation to a class that only knows the interface. You can follow DIP without DI (e.g., factories) and you can use DI in ways that violate DIP (e.g., injecting a concrete class directly).

↗ Related concepts

Comments 0

Discuss this page. Markdown supported. Be kind.

Loading…
Loading comments…