FLASH SALE
00:00:00
$49$9960% OFF
Back to Blog
SOLID Principles Design Principles Software Architecture OOP Interview Prep

SOLID Principles Explained: A Visual Guide with Real Examples

Vishnu Darshan Sanku
February 14, 2026
SOLID Principles Explained: A Visual Guide with Real Examples

SOLID Principles Explained: A Visual Guide

Every developer has written code they’re ashamed of. A class with 2,000 lines. A function called handleEverything(). A change in one file that mysteriously broke three others.

SOLID principles exist to prevent exactly that. They’re five guidelines—introduced by Robert C. Martin (Uncle Bob)—that tell you how to organize classes and interfaces so your code stays clean, flexible, and maintainable as it grows.

The best part? They’re not abstract theory. They’re practical rules you can apply today. And in LLD interviews, they’re what separates a “good enough” design from an excellent one.

S
Single Responsibility
One class, one job
O
Open / Closed
Extend, don't modify
L
Liskov Substitution
Subtypes must be swappable
I
Interface Segregation
Small, focused interfaces
D
Dependency Inversion
Depend on abstractions

How to Read This Guide

This guide is entirely visual — no code, just diagrams, comparisons, and real-world analogies. Each principle gets a before/after comparison showing what goes wrong when you violate it and what goes right when you follow it.


S — Single Responsibility Principle

S
Single Responsibility
A class should have one, and only one, reason to change.
Real-World Analogy
Think of a restaurant. The chef cooks. The waiter serves. The cashier handles payments. If the chef also had to serve tables and manage the register, the kitchen would be chaos. Each person has one responsibility, and the restaurant runs smoothly.

The Problem: The God Class

When a single class handles everything—data access, business logic, formatting, logging, email sending—it becomes a “God Class.” Change one thing, and everything breaks.

Violation: God Class
OrderManager
validateOrder()
calculateTotal()
applyDiscount()
saveToDatabase()
sendConfirmationEmail()
generateInvoicePDF()
updateInventory()
notifyWarehouse()
Changing email format requires touching the same class as pricing logic
Testing order validation requires setting up database connections
One bug in invoice generation can break the entire ordering flow
Multiple developers editing the same file causes constant merge conflicts
Correct: Focused Classes
OrderValidator
validate()
PricingService
calculateTotal()
applyDiscount()
OrderRepository
save()
findById()
EmailService
sendConfirmation()
InvoiceGenerator
generatePDF()
InventoryService
updateStock()
Each class has exactly one reason to change
Changes to email logic can't break pricing calculations
Each class can be tested independently with simple mocks
Different developers work on different files without conflicts

How It Works in Practice

Here’s how the focused classes interact — each doing one job, collaborating through clear interfaces:

The OrderController orchestrates the flow, but each service handles its own concern. Need to change how emails are sent? Touch EmailService — nothing else.

The Smell Test

Signs You're Following SRP

  • You can describe what a class does in one sentence without using “and”
  • Changes to one feature only affect one class
  • Your class names are specific: InvoiceGenerator, not Utilities
  • Unit tests are straightforward with minimal mocking

Signs You're Violating SRP

  • A class has more than 200 lines
  • You use words like “Manager”, “Handler”, “Processor”, “Utils” as class names
  • A change to the database layer requires editing a UI-related class
  • Unit tests need complex setup with many dependencies

Deep dive: Single Responsibility Principle


O — Open/Closed Principle

O
Open/Closed Principle
Open for extension, closed for modification.
Real-World Analogy
Think of a power strip. You can plug in new devices (extension) without rewiring the strip itself (modification). The strip's design is 'closed' — you never open it up. But it's 'open' to new devices through its standard outlets.

The Problem: The Growing Switch Statement

Every time a new requirement comes in, you modify existing working code. Each modification risks breaking what already works.

Violation: Modify to Extend
PaymentProcessor
processPayment(type)
if type == "credit" → ...
if type == "debit" → ...
if type == "upi" → ...
if type == "crypto" → ... (NEW!)
// Add more if-else for each new type
Adding crypto requires modifying the existing PaymentProcessor
Existing credit/debit tests must be re-run after every new type
Risk of accidentally breaking working payment types
The class grows endlessly with each new payment method
Correct: Extend Without Modifying
PaymentMethod (interface)
process(amount): Result
CreditCardPayment
process() → credit logic
DebitCardPayment
process() → debit logic
UPIPayment
process() → UPI logic
CryptoPayment (NEW!)
process() → crypto logic
Adding CryptoPayment requires zero changes to existing classes
Existing payment methods are untouched and stable
Each payment type is tested independently
New types are added by creating a new class, not editing an old one

How Extension Points Work

The interface acts as the “standard outlet” — existing code depends on the interface, not on specific implementations:

CheckoutService depends on the PaymentMethod interface. When you add Crypto, you only create one new class. CheckoutService doesn’t change. CreditCard doesn’t change. Nothing existing is touched.

Where You’ll See This in Interviews

In almost every LLD interview problem, the interviewer will ask: “What if we need to add a new type of X?” The Open/Closed Principle is how you answer:

  • Parking Lot: “Add a new vehicle type” → new class implementing Vehicle interface
  • Rate Limiter: “Add a new algorithm” → new class implementing RateLimiter interface
  • Elevator System: “Add a new dispatch strategy” → new class implementing DispatchStrategy

Deep dive: Open/Closed Principle


L — Liskov Substitution Principle

L
Liskov Substitution
Subtypes must be substitutable for their base types.
Real-World Analogy
A rental car company promises you a 'sedan.' Whether they give you a Toyota, Honda, or BMW, you expect it to drive the same way — steering wheel, pedals, gear shift all work as expected. If they gave you a 'sedan' that steered with a joystick, the substitution would break.

The Problem: The Lying Subclass

A class inherits from a parent but doesn’t actually behave like the parent. Code that works with the parent breaks when handed the child.

Violation: The Square-Rectangle Trap
Rectangle
width, height
setWidth(w)
setHeight(h)
area() → width × height
Square extends Rectangle
setWidth(w) → also sets height to w!
setHeight(h) → also sets width to h!
area() → side × side
Code that sets width=5, height=10 and expects area=50 gets area=100 with a Square
Square silently changes behavior of inherited methods
Any function expecting a Rectangle may break with a Square
Violates the principle: substitution changes the result
Correct: Honest Hierarchies
Shape (interface)
area(): number
perimeter(): number
Rectangle implements Shape
width, height
area() → width × height
Square implements Shape
side
area() → side × side
Rectangle and Square are siblings, not parent-child
Both fulfill the Shape contract honestly
Substituting one for another in Shape-accepting code works correctly
No hidden side effects from overridden methods

The Substitution Test

Here’s a visual way to think about it — if code works with the parent type, it must also work with any child type:

If your function calls bird.fly(), then Penguin violates LSP because it can’t fly. The fix: either don’t inherit from Bird, or redesign the hierarchy so fly() isn’t in the base type.

The Simple Rule

Ask yourself: “Can I swap a child object for a parent object everywhere the parent is used, without anything breaking or behaving unexpectedly?” If yes, you’re following LSP. If no, your hierarchy is wrong.

Deep dive: Liskov Substitution Principle


I — Interface Segregation Principle

I
Interface Segregation
No client should be forced to depend on methods it doesn't use.
Real-World Analogy
Think of restaurant menus. A fine dining restaurant doesn't give you one giant menu with breakfast, lunch, dinner, drinks, desserts, and catering all on one card. They give you the menu relevant to your meal. A breakfast customer shouldn't have to flip through 50 dinner options.

The Problem: The Fat Interface

One massive interface forces every implementation to deal with methods it doesn’t care about.

Violation: Fat Interface
Worker (interface)
work()
eat()
sleep()
attendMeeting()
writeReport()
Robot implements Worker
work() → ✅ does work
eat() → ❌ throws error (robots don't eat)
sleep() → ❌ throws error
attendMeeting() → ❌ throws error
writeReport() → ✅ generates report
Robot is forced to implement eat() and sleep() — nonsensical
Every new method added to Worker affects ALL implementations
Implementations throw errors for methods they can't support
Testing requires mocking methods the class doesn't actually use
Correct: Segregated Interfaces
Workable (interface)
work()
Feedable (interface)
eat()
sleep()
Reportable (interface)
writeReport()
Meetable (interface)
attendMeeting()
Robot implements Workable, Reportable
work() → ✅
writeReport() → ✅
No wasted methods!
Human implements All
work() → ✅
eat() → ✅
sleep() → ✅
attendMeeting() → ✅
Robot only implements what it actually does
Adding a new interface doesn't affect existing implementations
No dummy methods or thrown errors for unsupported operations
Each interface is small, focused, and meaningful

The Visual Difference

See how much cleaner the dependencies become when interfaces are segregated:

On the left, every worker depends on 5 methods. On the right, each worker picks only the interfaces it needs.

Real Interview Application

In a Notification Service design, don’t create one fat NotificationSender interface with sendEmail(), sendSMS(), sendPush(), sendSlack(). Instead, create separate interfaces: EmailSender, SMSSender, PushSender. Each channel implements only its own interface.

Deep dive: Interface Segregation Principle


D — Dependency Inversion Principle

D
Dependency Inversion
Depend on abstractions, not on concretions.
Real-World Analogy
Think of electrical outlets. Your laptop charger doesn't wire directly into the building's electrical system. It plugs into a standard outlet (the abstraction). This means your laptop works in any building with that outlet standard, and the building doesn't care what you plug in.

The Problem: Direct Dependencies

High-level business logic depends directly on low-level implementation details. Changing the database means rewriting the business logic.

Violation: Direct Coupling
OrderService (high-level)
Directly creates MySQLDatabase
Directly creates StripePayment
Directly creates SmtpEmailer
Knows all implementation details
MySQLDatabase (low-level)
MySQL-specific queries
Connection pooling
Schema details
Switching from MySQL to Postgres means rewriting OrderService
Can't test OrderService without a real MySQL database running
OrderService breaks if Stripe changes their API
High-level business logic is tangled with low-level details
Correct: Inverted Dependencies
OrderService (high-level)
Depends on Database interface
Depends on PaymentGateway interface
Depends on Notifier interface
Knows nothing about implementations
Database (interface)
save()
findById()
delete()
MySQLDatabase implements Database
MySQL-specific implementation
PostgresDatabase implements Database
Postgres-specific implementation
Switch databases by swapping the implementation — OrderService untouched
Test with a simple in-memory mock that implements the Database interface
Payment provider changes only affect one implementation class
Business logic is completely independent of infrastructure choices

The Dependency Direction

This is the core insight — notice how the arrows flip:

Without DIP: High-level points down to low-level (direct dependency). With DIP: Both high-level and low-level point toward the abstraction (inverted). The abstraction is the “standard outlet” that decouples everything.

Why This Is the Most Important Principle

DIP is the foundation of testable, maintainable software. Without it:

  • You can’t write unit tests (you’d need real databases, real APIs)
  • You can’t swap implementations (stuck with your initial choices forever)
  • You can’t work in parallel (teams are blocked by each other’s infrastructure decisions)

In LLD interviews, using interfaces to define dependencies is the single biggest signal that you’re a strong designer. Every problem — from parking lots to payment processors — benefits from this approach.

Deep dive: Dependency Inversion Principle


How the 5 Principles Work Together

SOLID isn’t five separate ideas — they reinforce each other. Here’s how they connect:

  • SRP gives you small, focused classes
  • Small classes are easier to extend without modification (OCP)
  • Extensions through inheritance must be substitutable (LSP)
  • To be substitutable, interfaces must be focused and minimal (ISP)
  • Focused interfaces become the abstractions that DIP depends on
  • DIP’s clean dependencies make it easy to keep classes single-responsibility

It’s a virtuous cycle. Following one principle naturally leads to following the others.


SOLID in LLD Interviews: A Cheat Sheet

Here’s how to demonstrate SOLID knowledge in your next interview:

When The Interviewer Says…Apply This PrincipleWhat To Do
”This class does too much”SRPSplit into focused classes, each with one responsibility
”What if we add a new type?”OCPDefine an interface; new types implement it without changing existing code
”Can we swap X for Y?”LSPEnsure Y behaves correctly everywhere X is used
”This interface is too big”ISPSplit into smaller interfaces; clients only depend on what they use
”How do you test this?”DIPDepend on interfaces; inject mock implementations in tests

The Interview Signal

You don’t need to name-drop “Liskov Substitution Principle” in an interview. Instead, naturally say things like: “I’ll define an interface here so we can add new types without modifying existing code” (OCP + DIP) or “I’ll keep this class focused on just the pricing logic” (SRP). The interviewer will recognize the principles from your design decisions.


The Anti-Patterns: What Happens When You Ignore SOLID

What it looks like: One class with 50+ methods that handles everything — business logic, database access, formatting, validation, notification.

Real-world analogy: A restaurant where one person cooks, serves, cleans, manages bookings, and handles complaints. Total burnout, constant errors.

How to fix: Ask for each method: “Does this belong in the same class?” If a class needs the word “and” to describe its purpose, it’s doing too much.


Key Takeaways

Remember These

  1. SRP — If your class name needs “and” to describe it, split it
  2. OCP — New features = new classes, not new if-else branches
  3. LSP — Every subclass must work wherever the parent works
  4. ISP — Many small interfaces beat one large interface
  5. DIP — Business logic talks to interfaces, not implementations
  6. They work together — Following one naturally leads to following the others
  7. In interviews — Don’t name-drop principles; demonstrate them through design decisions

“The goal of software architecture is to minimize the human resources required to build and maintain the required system.” — Robert C. Martin (Uncle Bob), the creator of SOLID

SOLID principles aren’t rules to follow blindly. They’re guidelines that steer you toward designs that are easy to understand, easy to change, and easy to extend. Master them visually first, then apply them instinctively — and your designs will speak for themselves.