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:
- S β Single Responsibility Principle
- O β Open/Closed Principle
- L β Liskov Substitution Principle
- I β Interface Segregation Principle
- 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
}
}
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) { /* ... */ }
}
π€ 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) { /* ... */ }
}
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
}
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;
}
}
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();
}
}
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
}
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
}
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 */ }
}
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() { /* ... */ }
}
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
}
}
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
}
}
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.
Top comments (0)