DEV Community

Cover image for What Does SOLID Mean?
Matheus Bernardes Spilari
Matheus Bernardes Spilari

Posted on

What Does SOLID Mean?

If you've spent any time learning about clean code or software architecture, you've probably come across the SOLID principles. These five principles, introduced by Robert C. Martin (Uncle Bob), aim to make your code more maintainable, flexible, and scalable.

In this article, we’ll break down what each letter in SOLID stands for, and walk through simple examples of each one.


🧱 What is SOLID?

SOLID is an acronym for five object-oriented 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

1. βœ… Single Responsibility Principle (SRP)

A class should have only one reason to change.

Each class should do one thing, and do it well. If a class has more than one responsibility, it's harder to maintain and more likely to break when you make changes.

❌ Bad Example:

public class Invoice {
    public void calculateTotal() {
        // logic here
    }

    public void printInvoice() {
        // printing logic
    }

    public void saveToDatabase() {
        // database logic
    }
}
Enter fullscreen mode Exit fullscreen mode

This class is doing too much: calculation, printing, and database access.

βœ… Good Example:

public class InvoiceCalculator {
    public void calculateTotal(Invoice invoice) { /* ... */ }
}

public class InvoicePrinter {
    public void print(Invoice invoice) { /* ... */ }
}

public class InvoiceRepository {
    public void save(Invoice invoice) { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

πŸ€” Does SRP mean "one method per class"?

No β€” that’s a common misconception.

The Single Responsibility Principle does not mean that a class should only have one method. It means the class should have one reason to change β€” in other words, a clearly defined responsibility.

For example, in a Spring Boot application, it's completely normal (and correct!) for a controller to have multiple methods, like this:

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping
    public List<User> listUsers() { /* ... */ }

    @PostMapping
    public User createUser(@RequestBody User user) { /* ... */ }

    @PutMapping("/{id}")
    public User updateUser(@PathVariable Long id, @RequestBody User user) { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

This controller has one responsibility: handling HTTP requests for the User resource.

Each method maps to a different route, but they all belong to the same context β€” user handling.

🚫 When SRP is broken:

If your controller also starts doing business logic, data validation, and database operations, then it starts to accumulate responsibilities:

public class UserController {
    // HTTP handling
    // + Business logic
    // + Validation
    // + Persistence
}
Enter fullscreen mode Exit fullscreen mode

In that case, you'd want to refactor and delegate:

  • Business rules β†’ Service layer
  • Validation β†’ Validator
  • Persistence β†’ Repository

So remember: SRP is about cohesive responsibility, not method count.


2. πŸ›‘ Open/Closed Principle (OCP)

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

You should be able to add new functionality without changing existing code.

❌ Bad Example:

public class DiscountService {
    public double getDiscount(String customerType) {
        if (customerType.equals("Regular")) return 0.1;
        else if (customerType.equals("Premium")) return 0.2;
        else return 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

If you need to support a new type of customer, you'll have to modify this method.

βœ… Good Example (Using Strategy Pattern):

public interface DiscountStrategy {
    double getDiscount();
}

public class RegularCustomer implements DiscountStrategy {
    public double getDiscount() { return 0.1; }
}

public class PremiumCustomer implements DiscountStrategy {
    public double getDiscount() { return 0.2; }
}

public class DiscountService {
    public double calculateDiscount(DiscountStrategy strategy) {
        return strategy.getDiscount();
    }
}
Enter fullscreen mode Exit fullscreen mode

You can now extend with new customer types without changing existing code.

Want to see a full example of the Strategy pattern in action? Check out this article where I break it down step by step.


3. 🧬 Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types.

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

❌ Bad Example:

public class Bird {
    public void fly() { /* flying logic */ }
}

public class Ostrich extends Bird {
    // can't fly, but inherits fly() anyway
}
Enter fullscreen mode Exit fullscreen mode

Using Ostrich in a context that expects a flying bird will cause problems.

βœ… Good Example:

public abstract class Bird { }

public interface FlyingBird {
    void fly();
}

public class Sparrow extends Bird implements FlyingBird {
    public void fly() { /* logic */ }
}

public class Ostrich extends Bird {
    // No fly() method
}
Enter fullscreen mode Exit fullscreen mode

Now, we can substitute FlyingBird only where flying is expected.


4. πŸͺͺ Interface Segregation Principle (ISP)

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

Split large interfaces into smaller, more specific ones.

❌ Bad Example:

public interface Worker {
    void work();
    void eat();
}

public class Robot implements Worker {
    public void work() { /* ok */ }
    public void eat() { /* meaningless */ }
}
Enter fullscreen mode Exit fullscreen mode

A robot doesn’t eat β€” but it's forced to implement eat().

βœ… Good Example:

public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public class Human implements Workable, Eatable {
    public void work() { /* ... */ }
    public void eat() { /* ... */ }
}

public class Robot implements Workable {
    public void work() { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

Each class only implements what it needs.


5. πŸ”„ Dependency Inversion Principle (DIP)

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

This encourages loosely coupled code.

❌ Bad Example:

public class MySQLDatabase {
    public void connect() { /* ... */ }
}

public class UserRepository {
    private MySQLDatabase db = new MySQLDatabase();

    public void saveUser(User user) {
        db.connect();
        // save logic
    }
}
Enter fullscreen mode Exit fullscreen mode

The repository is tightly coupled to MySQL.

βœ… Good Example:

public interface Database {
    void connect();
}

public class MySQLDatabase implements Database {
    public void connect() { /* ... */ }
}

public class UserRepository {
    private Database db;

    public UserRepository(Database db) {
        this.db = db;
    }

    public void saveUser(User user) {
        db.connect();
        // save logic
    }
}
Enter fullscreen mode Exit fullscreen mode

Now you can inject any Database implementation β€” perfect for testing and flexibility.

This principle can be a bit tricky to grasp at first. If you want a deeper, more practical explanation of Dependency Inversion with Spring Boot/Java examples, I wrote a full article about it here.


Conclusion

The SOLID principles are essential for writing clean, maintainable, and scalable object-oriented code. While they may seem abstract at first, practicing them over time leads to better architecture and fewer headaches as your project grows.

πŸ“ Reference

πŸ‘‹ Talk to me

Top comments (0)