Skip to content
Dev Dump

SOLID Principles

The SOLID principles are a foundational set of five object-oriented design rules.

  • S → Single Responsibility Principle (SRP)
  • O → Open/Closed Principle (OCP)
  • L → Liskov Substitution Principle (LSP)
  • I → Interface Segregation Principle (ISP)
  • D → Dependency Inversion Principle (DIP)

“A class should have one, and only one, reason to change.”

  • Goal: To create highly cohesive classes. If a class handles multiple responsibilities, changing one part of the logic might inadvertently break another.

Imagine an Employee class that holds personal details, calculates taxes, and writes data directly to a database.

public class Employee {
private String name;
private double salary;
// Responsibility 1: Business Logic
public double calculateTax() { ... }
// Responsibility 2: Database Logic
public void saveToDB() { ... }
// Responsibility 3: Presentation Logic
public void printReport() { ... }
}

This class is tightly coupled. If the database schema changes, or the tax law changes, or the reporting format changes, this single class must be modified.

Single Responsibility Principle Split the monolithic God Class into smaller, focused classes.

We delegate responsibilities to dedicated classes. This makes the code loosely coupled, easily testable, and limits the reasons to modify any single file.

// Handles only data
public class Employee {
private String name;
private double salary;
}
// Handles only business logic
public class TaxCalculator {
public double calculateTax(Employee e) { ... }
}
// Handles only database operations
public class EmployeeRepository {
public void save(Employee e) { ... }
}

“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”

  • Goal: To allow developers to add new features without altering existing, tested, and working code, thereby reducing the risk of introducing regressions.

A SalaryCalculator that uses conditional logic to determine behavior based on an employee type.

public class SalaryCalculator {
public double calculate(Employee e) {
if (e.getType().equals("PERMANENT")) {
return e.getBase() + e.getBonus();
} else if (e.getType().equals("CONTRACT")) {
return e.getHours() * e.getRate();
}
return 0;
}
}

Issue: If a new employee type (e.g., PartTime) is introduced, you must modify the calculate method, violating OCP.

Open/Closed Principle Use polymorphism. An interface defines the contract, and new features are added by creating new classes.

Create an interface and let child classes implement their specific logic. Now, adding a PartTimeEmployee requires zero changes to existing files.

public interface Employee {
double calculateSalary();
}
public class PermanentEmployee implements Employee {
public double calculateSalary() {
return base + bonus;
}
}
public class ContractEmployee implements Employee {
public double calculateSalary() {
return hours * rate;
}
}

“Objects of a superclass shall be replaceable with objects of its subclasses without breaking the application.”

  • Goal: To ensure inheritance is used correctly. If a program expects a parent class, it should be able to work flawlessly with any child class.

A ToyCar extending a Car class but failing to fulfill the contract of a “Car” because toys don’t use real fuel.

public class Car {
public void startEngine() { ... }
public void addFuel() { ... }
}
public class ToyCar extends Car {
@Override
public void addFuel() {
throw new UnsupportedOperationException("Toy cars don't use fuel!");
}
}

If a piece of code accepts a Car and calls .addFuel(), substituting it with a ToyCar will crash the application.

Liskov Substitution Principle Refactor the hierarchy so subclasses only inherit what they can actually fulfill.

public interface Vehicle {
void startEngine();
}
public class FuelVehicle implements Vehicle {
public void startEngine() { ... }
public void addFuel() { ... }
}
public class ToyVehicle implements Vehicle {
public void startEngine() { ... }
public void replaceBattery() { ... }
}

“No client should be forced to depend on methods it does not use.”

  • Goal: To avoid creating large, monolithic interfaces (“fat interfaces”). Interfaces should be small and highly specific.

A massive Machine interface forced onto a basic printer that cannot scan or fax.

public interface Machine {
void print();
void scan();
void fax();
}
public class BasicPrinter implements Machine {
public void print() { /* valid implementation */ }
public void scan() { throw new NotSupportedException(); }
public void fax() { throw new NotSupportedException(); }
}

Interface Segregation Principle Break the fat interface into smaller, role-specific interfaces.

public interface IPrinter { void print(); }
public interface IScanner { void scan(); }
public interface IFax { void fax(); }
// A basic printer only implements what it needs
public class BasicPrinter implements IPrinter {
public void print() { ... }
}
// A smart machine can implement multiple interfaces
public class AllInOneMachine implements IPrinter, IScanner, IFax {
public void print() { ... }
public void scan() { ... }
public void fax() { ... }
}

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

  • Goal: To decouple your application’s core logic from specific infrastructure details (like a specific database vendor or third-party API).

A high-level OrderService directly instantiating a low-level concrete MySQLRepository.

public class OrderService {
// Tight coupling to a specific database implementation
private MySQLRepository repository = new MySQLRepository();
public void checkout(Order order) {
repository.save(order);
}
}

Issue: If you want to migrate to a NoSQL database, you have to rewrite the OrderService. It is impossible to unit test OrderService without an actual MySQL database running.

Dependency Inversion Principle Introduce an interface layer. The high-level service depends on the interface, and the low-level database implements it.

// 1. Define the abstraction
public interface IRepository {
void save(Order order);
}
// 2. Low-level module implements abstraction
public class MySQLRepository implements IRepository {
public void save(Order order) { ... }
}
// 3. High-level module depends on abstraction via Dependency Injection
public class OrderService {
private IRepository repository;
// Injected via constructor
public OrderService(IRepository repository) {
this.repository = repository;
}
public void checkout(Order order) {
repository.save(order);
}
}