Panda Coding School
Back to Blog

SOLID Principles Explained with Real-World Examples | Complete Guide for Developers

Learn the SOLID principles with simple explanations, real-world analogies, and TypeScript examples. Improve your software design and write cleaner, scalable, maintainable code.

Panda Coding SchoolJune 2, 20267 min read

If you've been learning software engineering, you've probably heard developers say:

"This code violates SOLID principles."

Or:

"Let's refactor this using SOLID."

At first, SOLID may sound like another buzzword in software development. However, these five principles are some of the most important concepts for writing maintainable, scalable, and testable code.

Whether you're building a small application or a large enterprise system, understanding SOLID principles will help you create software that is easier to extend and less likely to break when requirements change.

In this article, we'll break down each SOLID principle with simple explanations, real-world analogies, and practical TypeScript examples.


What is SOLID?

SOLID is an acronym representing five software design principles:

LetterPrinciple
SSingle Responsibility Principle
OOpen/Closed Principle
LLiskov Substitution Principle
IInterface Segregation Principle
DDependency Inversion Principle

These principles were introduced by software engineer Robert C. Martin and are widely used in object-oriented programming.

Let's explore them one by one.


S — Single Responsibility Principle (SRP)

Definition

A class should have only one reason to change.

In simple words:

One class should have one job.

Bad Example

Imagine a User class doing everything:

class UserService {
  createUser() {
    console.log("User created");
  }
 
  sendEmail() {
    console.log("Sending email");
  }
 
  generateReport() {
    console.log("Generating report");
  }
}

This class handles:

  • User management
  • Email functionality
  • Report generation

Three different responsibilities.

If the email system changes, we must modify this class.

If reporting changes, we must modify this class again.


Better Approach

Separate responsibilities.

class UserService {
  createUser() {
    console.log("User created");
  }
}
 
class EmailService {
  sendEmail() {
    console.log("Email sent");
  }
}
 
class ReportService {
  generateReport() {
    console.log("Report generated");
  }
}

Now each class has a single responsibility.


Real-World Example

Think about a restaurant.

  • Chef cooks food.
  • Cashier handles payments.
  • Waiter serves customers.

If the chef starts handling billing and serving tables, chaos begins.

The same applies to software.


O — Open/Closed Principle (OCP)

Definition

Software entities should be:

  • Open for extension
  • Closed for modification

This means we should be able to add new functionality without changing existing code.


Bad Example

class PaymentService {
  process(type: string) {
    if (type === "credit-card") {
      console.log("Credit Card Payment");
    }
 
    if (type === "upi") {
      console.log("UPI Payment");
    }
  }
}

Now imagine adding:

  • PayPal
  • Stripe
  • Crypto

You'll keep modifying this class forever.


Better Approach

interface PaymentMethod {
  pay(): void;
}
 
class CreditCardPayment implements PaymentMethod {
  pay() {
    console.log("Credit Card Payment");
  }
}
 
class UpiPayment implements PaymentMethod {
  pay() {
    console.log("UPI Payment");
  }
}
 
class PaymentService {
  process(payment: PaymentMethod) {
    payment.pay();
  }
}

Adding a new payment method now requires creating a new class instead of changing existing code.


Real-World Example

A smartphone charger port follows OCP.

You can connect new accessories without redesigning the phone every time.


L — Liskov Substitution Principle (LSP)

Definition

Objects of a subclass should be replaceable with objects of the parent class without breaking the application.

Simply put:

If class B extends class A, we should be able to use B anywhere A is expected.


Bad Example

class Bird {
  fly() {
    console.log("Flying");
  }
}
 
class Penguin extends Bird {
  fly() {
    throw new Error("Penguins cannot fly");
  }
}

This breaks expectations.

A penguin is not a valid replacement for a flying bird.


Better Approach

class Bird {}
 
class FlyingBird extends Bird {
  fly() {
    console.log("Flying");
  }
}
 
class Sparrow extends FlyingBird {}
 
class Penguin extends Bird {}

Now the hierarchy reflects reality.


Real-World Example

Imagine renting a car.

If a customer receives a bicycle instead, the contract is broken because the substitute doesn't behave as expected.


I — Interface Segregation Principle (ISP)

Definition

Clients should not be forced to depend on interfaces they do not use.

In simple words:

Don't create giant interfaces.


Bad Example

interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
}

Now a robot worker must implement:

class Robot implements Worker {
  work() {}
 
  eat() {}
 
  sleep() {}
}

Robots don't eat or sleep.

This design is incorrect.


Better Approach

interface Workable {
  work(): void;
}
 
interface Eatable {
  eat(): void;
}
 
interface Sleepable {
  sleep(): void;
}
 
class Human implements Workable, Eatable, Sleepable {
  work() {}
  eat() {}
  sleep() {}
}
 
class Robot implements Workable {
  work() {}
}

Each class implements only what it needs.


Real-World Example

When applying for a driver's license, you're only required to learn driving-related skills.

You aren't forced to learn airplane navigation.


D — Dependency Inversion Principle (DIP)

Definition

High-level modules should not depend on low-level modules.

Both should depend on abstractions.


Bad Example

class MySQLDatabase {
  save() {
    console.log("Saved to MySQL");
  }
}
 
class UserService {
  private db = new MySQLDatabase();
 
  createUser() {
    this.db.save();
  }
}

Problem:

If we switch to PostgreSQL or MongoDB, we must change UserService.


Better Approach

interface Database {
  save(): void;
}
 
class MySQLDatabase implements Database {
  save() {
    console.log("Saved to MySQL");
  }
}
 
class MongoDatabase implements Database {
  save() {
    console.log("Saved to MongoDB");
  }
}
 
class UserService {
  constructor(private db: Database) {}
 
  createUser() {
    this.db.save();
  }
}
 
const userService = new UserService(new MongoDatabase());

Now UserService depends on an abstraction rather than a specific database.


Real-World Example

Consider a wall socket.

Your laptop depends on electricity, not on a specific power plant.

Whether electricity comes from solar, hydro, or nuclear power doesn't matter.

The socket acts as the abstraction.


How SOLID Helps in Real Projects

Following SOLID principles leads to:

  • Easier maintenance
  • Cleaner architecture
  • Better unit testing
  • Easier scalability
  • Reduced code duplication
  • Better team collaboration
  • Lower risk when introducing new features

These principles are heavily used in:

  • NestJS
  • Angular
  • React
  • Spring Boot
  • ASP.NET Core
  • Django
  • Enterprise applications

Common Mistake Developers Make

Many developers try to apply SOLID everywhere.

Remember:

SOLID is a guideline, not a rulebook.

For small projects, simple code is often better than over-engineered code.

As systems grow, SOLID becomes increasingly valuable because it helps manage complexity.

Focus on understanding the problem first, then apply the appropriate principle.


Final Thoughts

SOLID principles are not about writing more code. They're about writing code that can evolve without becoming a nightmare to maintain.

Here's a quick recap:

PrincipleKey Idea
Single ResponsibilityOne class, one job
Open/ClosedExtend without modifying
Liskov SubstitutionSubclasses should behave correctly
Interface SegregationSmall focused interfaces
Dependency InversionDepend on abstractions

Mastering SOLID principles will make you a better software engineer and help you design systems that remain flexible as requirements change.

The next time you're writing a class, ask yourself:

"Is this class doing too much?"

That simple question alone can dramatically improve the quality of your code.

Happy Coding! 🚀

Enjoyed this article?

Get more AI engineering insights delivered to your inbox.