Skip to main content

7.6 SOLID Principles

SOLID is an acronym for five core principles of object-oriented design, coined by Robert C. Martin ("Uncle Bob"). Following these principles produces code that is resilient to change, easy to understand, and highly reusable.

S - Single Responsibility Principle

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

A class should have only one responsibility (function). When multiple responsibilities are mixed, changing one affects the others.

// ❌ SRP violation: one class handles everything related to users
class User {
String name;
String email;

void saveToDatabase() { /* DB logic */ } // DB responsibility
void sendWelcomeEmail() { /* email logic */ } // email responsibility
String formatUserInfo() { return name + " <" + email + ">"; } // format responsibility
}

// ✅ SRP compliant: separate each responsibility into its own class
class User {
String name;
String email;
}

class UserRepository { // DB responsibility
void save(User user) { System.out.println("Saved to DB: " + user.name); }
}

class EmailService { // email responsibility
void sendWelcome(User user) { System.out.println("Email sent: " + user.email); }
}

class UserFormatter { // format responsibility
String format(User user) { return user.name + " <" + user.email + ">"; }
}

O - Open/Closed Principle

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

New functionality should be added by extending existing code, not modifying it.

// ❌ OCP violation: adding a new discount type requires modifying calculateDiscount()
class DiscountCalculator {
double calculateDiscount(String type, double price) {
if (type.equals("VIP")) return price * 0.2;
if (type.equals("STUDENT")) return price * 0.1;
// Must edit here every time a new type is added!
return 0;
}
}

// ✅ OCP compliant: extend via interface
interface DiscountPolicy {
double calculate(double price);
}

class VipDiscount implements DiscountPolicy {
public double calculate(double price) { return price * 0.2; }
}

class StudentDiscount implements DiscountPolicy {
public double calculate(double price) { return price * 0.1; }
}

class SeniorDiscount implements DiscountPolicy { // new type added: no change to existing code!
public double calculate(double price) { return price * 0.15; }
}

class OrderService {
// Accepts any DiscountPolicy — closed for modification
double finalPrice(double price, DiscountPolicy policy) {
return price - policy.calculate(price);
}
}

L - Liskov Substitution Principle

"Subtypes must be substitutable for their base types."

Code using a parent type must work correctly when any child type is substituted in.

// ❌ LSP violation: Square inheriting Rectangle breaks behavior
class Rectangle {
protected int width, height;
void setWidth(int w) { this.width = w; }
void setHeight(int h) { this.height = h; }
int area() { return width * height; }
}

class Square extends Rectangle {
@Override void setWidth(int w) { this.width = this.height = w; } // sets both
@Override void setHeight(int h) { this.width = this.height = h; }
}

// Code expecting Rectangle, given a Square:
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(10);
System.out.println(r.area()); // Expected: 50, Actual: 100 ← LSP violation!

// ✅ LSP compliant: design with a common interface
interface Shape { int area(); }
class Rectangle implements Shape { /* ... */ }
class Square implements Shape { /* ... */ }

I - Interface Segregation Principle

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

Many small, focused interfaces are better than one large interface.

// ❌ ISP violation: robots must implement eat() and sleep()
interface Worker {
void work();
void eat(); // robots don't eat...
void sleep(); // robots don't sleep...
}

class RobotWorker implements Worker {
public void work() { System.out.println("Robot working."); }
public void eat() { throw new UnsupportedOperationException("Robots don't eat"); }
public void sleep() { throw new UnsupportedOperationException("Robots don't sleep"); }
}

// ✅ ISP compliant: split interfaces by role
interface Workable { void work(); }
interface Eatable { void eat(); }
interface Sleepable { void sleep(); }

class HumanWorker implements Workable, Eatable, Sleepable {
public void work() { System.out.println("Human working."); }
public void eat() { System.out.println("Human eating."); }
public void sleep() { System.out.println("Human sleeping."); }
}

class RobotWorker implements Workable { // only what's needed
public void work() { System.out.println("Robot working."); }
}

D - Dependency Inversion Principle

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

Depend on interfaces (abstractions), not concrete implementation classes.

// ❌ DIP violation: high-level module directly depends on low-level implementation
class MySQLDatabase {
void save(String data) { System.out.println("Saving to MySQL: " + data); }
}

class UserService {
private MySQLDatabase db = new MySQLDatabase(); // directly depends on concrete class!

void saveUser(String user) { db.save(user); }
// Switching MySQL to PostgreSQL requires modifying UserService
}

// ✅ DIP compliant: depend on abstraction
interface Database {
void save(String data);
}

class MySQLDatabase implements Database {
public void save(String data) { System.out.println("MySQL: " + data); }
}

class PostgreSQLDatabase implements Database {
public void save(String data) { System.out.println("PostgreSQL: " + data); }
}

class UserService {
private final Database db; // depends on abstraction

UserService(Database db) { // constructor injection (DI)
this.db = db;
}

void saveUser(String user) { db.save(user); }
}

// Usage: inject the concrete implementation from outside
// (Spring does this automatically!)
UserService service1 = new UserService(new MySQLDatabase());
UserService service2 = new UserService(new PostgreSQLDatabase());

Pro Tip

When to apply SOLID:

SOLID proves its worth as projects grow. Start simple, then refactor when you see these signals:

  • SRP: A class has more than one reason to change → split it
  • OCP: Adding new features requires editing existing classes → add abstractions
  • LSP: Overridden methods throw exceptions or have empty implementations → reconsider inheritance
  • ISP: Implementing classes littered with throw new UnsupportedOperationException() → split interfaces
  • DIP: new ConcreteClass() scattered everywhere → use interfaces + dependency injection

In practice, Spring's @Autowired, @Service, and @Repository handle DIP and DI automatically.