DEV Community

Cover image for SOLID Principles: Writing Code That Survives the Real World
Azna Aroos
Azna Aroos

Posted on

SOLID Principles: Writing Code That Survives the Real World

Writing software is not just about making things work.
It’s about making things work today, tomorrow, and one year from now.

Last week, we explored Event-Driven Architecture — how systems communicate and scale.
This week, we’re stepping into something even more foundational:

SOLID Principles

SOLID is not a framework.
It’s not a library.
It’s a way of thinking.

Each letter in SOLID represents a principle that helps us design clean, flexible, and maintainable software.

Before we go technical, imagine yourself as a kid with a lot of toys:

One toy should do one thing.

A spoon is for eating.

A toothbrush is for brushing teeth.

If your spoon also tried to brush your teeth, that would be weird, right?
That’s where the first principle comes in.

S – Single Responsibility Principle (SRP)

One thing = One job

A class should have only one responsibility.
It should have only one reason to change.

If a class handles:

user registration

sending emails

saving data to the database

…that’s too much responsibility.

Just like a spoon shouldn’t brush your teeth, a class shouldn’t try to do everything.

In the real world:

A chef cooks.

A driver drives.

A teacher teaches.

Clear roles. Clear responsibilities.

classDiagram
    class UserService {
        +registerUser()
    }
    class EmailService {
        +sendEmail()
    }
    UserService --> EmailService
Enter fullscreen mode Exit fullscreen mode

Figure 1: Single Responsibility Principle (SRP) – Each class has only one responsibility.

O – Open/Closed Principle (OCP)

You can add new toys to your toy box without breaking your old ones.

That means:

You can extend behavior without modifying existing code.

Instead of changing old code every time you need something new, you extend it.
Extend. Don’t modify.

In the real world:

You don’t break your house walls to add a new device.

You plug something into an existing socket.

Good software design allows growth without destruction.

classDiagram
    class Payment {
        <<interface>>
        +pay(amount)
    }
    class CreditCardPayment {
        +pay(amount)
    }
    class PayPalPayment {
        +pay(amount)
    }
    class PaymentProcessor {
        +process(payment)
    }
    Payment <|.. CreditCardPayment
    Payment <|.. PayPalPayment
    PaymentProcessor --> Payment

Enter fullscreen mode Exit fullscreen mode

Figure 2: Open–Closed Principle (OCP) – Extend functionality without modifying existing code.

L – Liskov Substitution Principle (LSP)

If you replace your chocolate milk with strawberry milk, it should still behave like milk.

If something is a “type of” something else, it should behave properly as that type.

Example:

We say:

“A penguin is a bird.”

But we also say:

“All birds can fly.”

Now something feels wrong — penguins can’t fly.

The problem is not the penguin, it’s our design assumption.

Rule: Don’t force subclasses to break expectations.
If a subclass cannot fully behave like its parent, your inheritance design is wrong.

classDiagram
    classDiagram
    class Bird
    class FlyingBird {
        <<interface>>
        +fly()
    }
    class Sparrow
    class Penguin
    FlyingBird <|.. Sparrow
    Bird <|-- Sparrow
    Bird <|-- Penguin


Enter fullscreen mode Exit fullscreen mode

Figure 3: Liskov Substitution Principle (LSP) – Subtypes must behave like their parent types.

I – Interface Segregation Principle (ISP)

In simple words: Don’t force someone to do things they don’t need to do.

If you give a kid responsibilities like:

Cook

Drive

…that’s too much. Give only what they actually need.

Example:

A TV remote has only TV buttons.

An AC remote has only AC buttons.

You don’t mix everything into one giant remote.

In software, large interfaces are like giant remotes.
Small, specific interfaces are clean and focused.

classDiagram
    class Workable {
        <<interface>>
        +work()
    }
    class Eatable {
        <<interface>>
        +eat()
    }
    class Human
    class Robot
    Workable <|.. Human
    Workable <|.. Robot
    Eatable <|.. Human



Enter fullscreen mode Exit fullscreen mode

Figure 4: Interface Segregation Principle (ISP)– Don’t force classes to implement methods they don’t need.

D – Dependency Inversion Principle (DIP)

If you love only red cars, when that car breaks, your heart breaks too.

But if you love cars in general, you can easily replace one with another.

In software: Depend on abstractions, not concrete implementations.

You plug devices into a wall socket, not directly into a specific power plant.
The socket is the abstraction; the power plant can change.

When high-level modules depend on abstractions:

Systems become flexible

Components become replaceable

Code becomes easier to maintain

classDiagram
    class Database {
        <<interface>>
        +save()
    }
    class MySQLDatabase
    class MongoDatabase
    class OrderService {
        +processOrder()
    }
    Database <|.. MySQLDatabase
    Database <|.. MongoDatabase
    OrderService --> Database




Enter fullscreen mode Exit fullscreen mode

Figure 5: Dependency Inversion Principle (DIP) – Depend on abstractions, not concrete implementations.

Final Thoughts

SOLID principles are not just theory.
They help you write code that:

Is easier to understand

Is easier to extend

Is easier to maintain

Doesn’t break easily

Good design today saves you from pain tomorrow.
When you follow SOLID, your software grows cleanly — just like a well-organized toy box.

💬 What About You?

Have you ever worked on a project where poor design caused problems later?
Which SOLID principle do you struggle with the most?
Let’s discuss in the comments 👇

Top comments (0)