DEV Community

Cover image for SOLID Principles in JavaScript — Explained in Simple Words for Beginners
SAURAV KUMAR
SAURAV KUMAR

Posted on

SOLID Principles in JavaScript — Explained in Simple Words for Beginners

When we first start coding, most of us care about one thing:

make it work.

And honestly, that is fine.

But after some time, projects grow.

A small file becomes a bigger module.
A simple feature gets more conditions.
One class starts handling too many things.
And suddenly, even a small change feels risky.

That is where SOLID principles become useful.

SOLID is a set of 5 design principles that help us write code that is:

  • cleaner
  • easier to maintain
  • easier to extend
  • easier to test

If the term sounds intimidating, don’t worry.

The names are bigger than the actual ideas.

Let’s break them down one by one in very simple words.


What is SOLID?

SOLID stands for:

  • S — Single Responsibility Principle
  • O — Open/Closed Principle
  • L — Liskov Substitution Principle
  • I — Interface Segregation Principle
  • D — Dependency Inversion Principle

At first, these can feel like interview-only terms.

But in reality, they are very practical.

They help us avoid messy code as our application grows.


Why do SOLID principles matter?

Without good design, code usually starts showing problems like these:

  • one class does too many things
  • adding a new feature means touching old stable code
  • child classes break assumptions made by parent classes
  • classes are forced to implement methods they do not need
  • business logic becomes tightly coupled to implementation details

SOLID helps reduce these problems.

It is not about making code “fancy”.

It is about making code easier to work with later.

Now let’s understand each principle.


S — Single Responsibility Principle

What does it mean?

The Single Responsibility Principle says:

A class, function, or module should have only one responsibility.

Another common way to say it is:

It should have only one reason to change.

This is the line that makes SRP easier to understand.

If one piece of code changes for many different reasons, it is probably handling too many responsibilities.


Example

class UserManager {
  validateUser(user) {
    return user.name && user.email;
  }

  saveUser(user) {
    console.log("Saving user to database");
  }

  sendWelcomeEmail(user) {
    console.log("Sending welcome email");
  }
}
Enter fullscreen mode Exit fullscreen mode

This class is doing 3 different jobs:

  • validating data
  • saving data
  • sending email

That means it has multiple reasons to change.

So it violates SRP.


Better version

class UserValidator {
  validate(user) {
    return user.name && user.email;
  }
}

class UserRepository {
  save(user) {
    console.log("Saving user to database");
  }
}

class EmailService {
  sendWelcomeEmail(user) {
    console.log("Sending welcome email");
  }
}
Enter fullscreen mode Exit fullscreen mode

Now each class has one clear job.

That makes the code easier to test, understand, and maintain.

Easy memory line

One unit of code = one job = one reason to change


O — Open/Closed Principle

What does it mean?

The Open/Closed Principle says:

Code should be open for extension, but closed for modification.

In simple words:

We should be able to add new behavior without repeatedly changing old stable code.


Example

class PaymentProcessor {
  pay(method, amount) {
    if (method === "card") {
      console.log("Paying by card:", amount);
    } else if (method === "upi") {
      console.log("Paying by UPI:", amount);
    } else if (method === "paypal") {
      console.log("Paying by PayPal:", amount);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This works in the beginning.

But what happens later when you want to add:

  • wallet
  • net banking
  • gift card

You keep editing the same class again and again.

That is where the code starts becoming harder to maintain.


Better version

class CardPayment {
  pay(amount) {
    console.log("Paying by card:", amount);
  }
}

class UpiPayment {
  pay(amount) {
    console.log("Paying by UPI:", amount);
  }
}

class PaypalPayment {
  pay(amount) {
    console.log("Paying by PayPal:", amount);
  }
}

class PaymentProcessor {
  constructor(paymentMethod) {
    this.paymentMethod = paymentMethod;
  }

  pay(amount) {
    this.paymentMethod.pay(amount);
  }
}
Enter fullscreen mode Exit fullscreen mode

Usage:

const processor = new PaymentProcessor(new UpiPayment());
processor.pay(500);
Enter fullscreen mode Exit fullscreen mode

Now if you want a new payment type, you can add a new class without touching PaymentProcessor.

That is the main idea of OCP.

Easy memory line

Add new behavior without repeatedly rewriting old stable logic


L — Liskov Substitution Principle

What does it mean?

The Liskov Substitution Principle says:

A child class should be able to replace its parent class without breaking expected behavior.

In simple words:

If code works with the parent, it should also work correctly with the child.

This principle is more about behavior than syntax.


Example

class Bird {
  fly() {
    console.log("Flying");
  }
}

class Sparrow extends Bird {
  fly() {
    console.log("Sparrow flying");
  }
}

class Penguin extends Bird {
  fly() {
    throw new Error("Penguins cannot fly");
  }
}
Enter fullscreen mode Exit fullscreen mode

Now suppose we have this function:

function makeBirdFly(bird) {
  bird.fly();
}
Enter fullscreen mode Exit fullscreen mode

This works for Sparrow.

But it breaks for Penguin.

That means Penguin cannot safely replace Bird in this design.

So this is an LSP violation.


Better version

Instead of assuming every bird can fly, we can design it more honestly:

class Bird {
  eat() {
    console.log("Eating");
  }
}

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

class Sparrow extends FlyingBird {
  fly() {
    console.log("Sparrow flying");
  }
}

class Penguin extends Bird {
  swim() {
    console.log("Penguin swimming");
  }
}
Enter fullscreen mode Exit fullscreen mode

Now the design matches real behavior better.

Easy memory line

A child should not break the expectations of the parent


I — Interface Segregation Principle

What does it mean?

The Interface Segregation Principle says:

A class should not be forced to depend on methods it does not use.

In simple words:

Keep contracts small and focused.

Even if you are using JavaScript and not always writing formal interfaces, the idea still matters a lot.


Example

class Machine {
  print() {}
  scan() {}
  fax() {}
}

class BasicPrinter extends Machine {
  print() {
    console.log("Printing");
  }

  scan() {
    throw new Error("Not supported");
  }

  fax() {
    throw new Error("Not supported");
  }
}
Enter fullscreen mode Exit fullscreen mode

This is bad design.

Why?

Because BasicPrinter is forced to implement:

  • scan()
  • fax()

even though it does not support them.

That is exactly what ISP tries to avoid.


Better version

class Printer {
  print() {}
}

class Scanner {
  scan() {}
}

class FaxMachine {
  fax() {}
}

class BasicPrinter extends Printer {
  print() {
    console.log("Printing");
  }
}
Enter fullscreen mode Exit fullscreen mode

Now the contract is much smaller and more relevant.

Easy memory line

Do not force unused methods on a class


D — Dependency Inversion Principle

What does it mean?

The Dependency Inversion Principle says:

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

That sounds formal, so let’s simplify it.

In simple words:

Important logic should not be tightly tied to one specific implementation.

Or even simpler:

Depend on a contract, not on a concrete class.


Example

class MySQLDatabase {
  save(data) {
    console.log("Saved to MySQL", data);
  }
}

class UserService {
  constructor() {
    this.db = new MySQLDatabase();
  }

  createUser(user) {
    this.db.save(user);
  }
}
Enter fullscreen mode Exit fullscreen mode

This works, but UserService is tightly coupled to MySQLDatabase.

If later you want PostgreSQL or MongoDB, you must modify UserService.

That is not flexible.


Better version

class MySQLDatabase {
  save(data) {
    console.log("Saved to MySQL", data);
  }
}

class PostgreSQLDatabase {
  save(data) {
    console.log("Saved to PostgreSQL", data);
  }
}

class UserService {
  constructor(database) {
    this.database = database;
  }

  createUser(user) {
    this.database.save(user);
  }
}
Enter fullscreen mode Exit fullscreen mode

Usage:

const userService = new UserService(new PostgreSQLDatabase());
userService.createUser({ name: "Saurav" });
Enter fullscreen mode Exit fullscreen mode

Now UserService does not care about the specific database.

That makes the code more flexible and easier to test.

Easy memory line

Main logic should not be tightly tied to implementation details


Quick recap

Let’s revise all 5 quickly:

  • SRP → one responsibility, one reason to change
  • OCP → extend behavior without repeatedly modifying old stable code
  • LSP → child should safely replace parent
  • ISP → don’t force unused methods
  • DIP → depend on abstractions, not concrete implementations

Final thoughts

When I first started revising SOLID, I felt the names were harder than the concepts.

But after breaking them down one by one, I realized they are actually very practical.

These principles are not about writing over-engineered code.

They are about writing code that stays manageable when your project grows.

You do not need to apply SOLID everywhere blindly.

But if your code is becoming harder to maintain, harder to extend, or full of side effects, SOLID gives you a very useful way to think.


👋 About Me

Hi, I’m Saurav Kumar.

I enjoy learning, building, and writing about web development in simple words, especially topics that are useful for beginners and developers preparing for interviews.

Right now, I’m also focusing on improving my understanding of core concepts like JavaScript, OOP, system design, and software engineering fundamentals.

Let's connect!

If you found this helpful, share it with a friend learning JavaScript — it might help them too.

Until next time, keep coding and keep learning 🚀

Top comments (0)