# Writing Better Code with SOLID Principles

> A practical guide to the five SOLID principles, with simple before-and-after examples showing how each one makes code easier to change, test and extend.

- Author: Tai Pham Phu
- Published: 14 Mar 2026
- Updated: 9 Oct 2026
- Tags: System Design, Software Design
- URL: https://phamphutai.com/2026/03/14/writing-better-code-with-solid-principles/

Software systems grow quickly. Without good design practices, codebases become hard to maintain, extend and debug. One of the most influential sets of guidelines for writing maintainable object-oriented code is **SOLID**.

SOLID is a set of five principles, introduced by Robert C. Martin, that help developers build flexible, understandable and scalable systems.

In this post, we'll walk through each principle and see how it leads to better software design.

## What is SOLID?

**SOLID** is an acronym for five design principles:

1.  **S** — Single Responsibility Principle
2.  **O** — Open/Closed Principle
3.  **L** — Liskov Substitution Principle
4.  **I** — Interface Segregation Principle
5.  **D** — Dependency Inversion Principle

Together, they encourage code that is **loosely coupled and highly cohesive**.

## 1\. Single Responsibility Principle (SRP)

**A class should have only one reason to change.**

In other words, each class should handle **only one responsibility**.

#### Bad Example

```
class UserService {
  createUser(user) {
    // save user to database
  }

  sendWelcomeEmail(user) {
    // send email
  }

  generateReport() {
    // analytics logic
  }
}
```

This class handles:

-   user persistence
-   email communication
-   reporting

That's too many responsibilities for one class.

#### Better Design

```
class UserRepository {
  save(user) {}
}
class EmailService {
  sendWelcomeEmail(user) {}
}
class ReportService {
  generateUserReport() {}
}
```

Each class now has **one clear responsibility**.

Benefits:

-   easier testing
-   better maintainability
-   clearer architecture

## 2\. Open/Closed Principle (OCP)

**Software entities should be open for extension but closed for modification.**

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

#### Bad Example

```
function getDiscount(customerType) {
  if (customerType === "regular") return 0.1
  if (customerType === "premium") return 0.2
}
```

Every time a new customer type is added, this function has to change.

#### Better Design

```
class Discount {
  getDiscount() {}
}

class RegularCustomer extends Discount {
  getDiscount() { return 0.1 }
}

class PremiumCustomer extends Discount {
  getDiscount() { return 0.2 }
}
```

Now you can extend the system **without modifying existing logic**.

## 3\. Liskov Substitution Principle (LSP)

This principle is named after Barbara Liskov, who proposed it.

**Objects of a superclass should be replaceable with objects of its subclasses without breaking the program.**

#### Bad Example

```
class Bird {
  fly() {}
}

class Penguin extends Bird {
  fly() {
    throw new Error("Penguins cannot fly")
  }
}
```

This violates LSP because a `Penguin` cannot stand in for a `Bird` everywhere a `Bird` is expected.

#### Better Design

```
class Bird {}

class FlyingBird extends Bird {
  fly() {}
}

class Penguin extends Bird {}
```

Now each class reflects its **true behavior**.

## 4\. Interface Segregation Principle (ISP)

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

Instead of one large interface, create **smaller, more specific ones**.

#### Bad Example

```
interface Worker {
  work()
  eat()
}
```

A robot worker doesn't need `eat()`.

#### Better Design

```
interface Workable {
  work()
}

interface Eatable {
  eat()
}
```

Classes implement only what they need.

Benefits:

-   less coupling
-   more flexible design

## 5\. Dependency Inversion Principle (DIP)

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

#### Bad Example

```
class OrderService {
  constructor() {
    this.paymentProcessor = new StripePayment()
  }
}
```

`OrderService` is tightly coupled to one specific implementation.

#### Better Design

```
class OrderService {
  constructor(paymentProcessor) {
    this.paymentProcessor = paymentProcessor
  }
}
```

Now you can inject:

-   Stripe
-   PayPal
-   a mock service for tests

This is the foundation of the **Dependency Injection** used in frameworks like Spring and Angular. Applied to a whole backend rather than a single class, the same idea becomes the "dependencies point inward" rule at the heart of [Clean Architecture](https://phamphutai.com/2026/03/14/clean-architecture-in-practice-how-to-structure-large-backend-systems/).

## Why SOLID Matters

Applying SOLID principles leads to:

-   cleaner architecture
-   easier testing
-   better scalability
-   less technical debt
-   more reusable components

In large systems, ignoring these principles often results in **fragile codebases that are hard to change**.

## When NOT to Use SOLID

SOLID is powerful, but it's easy to **overuse**.

For small scripts or prototypes:

-   over-abstraction can slow development down
-   too many classes can make code harder to read

The key is **balance**.

## When This Matters for a Startup

In a startup, speed of learning usually matters more than elegant code. A class hierarchy built for customer types you don't have yet is a guess, and early guesses are often wrong. A plain `if` statement is fine until the third or fourth case shows you what actually varies.

Some principles still earn their keep early, because they cost almost nothing. Single Responsibility keeps files small enough to understand quickly. Dependency Inversion matters wherever you talk to an outside service, like a payment provider, a booking platform or an LLM API, because those are the parts you'll want to swap, mock in tests, or survive when a vendor changes its API.

A practical approach: write the simple version first, then apply SOLID where change keeps hurting. Two or three similar edits in the same place is a good signal. For the parts of a product that customers depend on every day, being correct and easy to change beats being clever, a point I make from the business side in [Boring Businesses Make the Best Businesses](https://phamphutai.com/2026/10/07/boring-businesses-make-the-best-businesses/).

## Final Thoughts

SOLID principles are not strict rules but **guidelines for writing better software**. Applied thoughtfully, they help you build systems that are easier to maintain, extend and understand.

Whether you're building enterprise systems or personal projects, learning SOLID well will noticeably improve your design skills.
