Skip to content

SE 04 SOLID Principles

Dr M H B Ariyaratne edited this page Jun 8, 2026 · 1 revision

SE-04: SOLID Principles

Part of the Software Engineering Principles series


Overview

SOLID is a set of five design principles introduced by Robert C. Martin ("Uncle Bob") that guide object-oriented class design. Applied together, they produce code that is easier to understand, test, extend, and maintain.

Each letter stands for one principle:

Letter Principle Core idea
S Single Responsibility A class should have one reason to change
O Open-Closed Open for extension, closed for modification
L Liskov Substitution Subtypes must be substitutable for their base types
I Interface Segregation Clients should not depend on methods they do not use
D Dependency Inversion Depend on abstractions, not concretions

S — Single Responsibility Principle (SRP)

A class should have only one reason to change.

A "reason to change" corresponds to a distinct area of responsibility. If a class is responsible for both business logic and data access and formatting, it will need to change whenever any of those three concerns change — even if the others are stable.

Violation

public class PatientReport {
    // Responsibility 1: fetch data from database
    public List<Patient> getAdmittedPatients() {
        // database query here
    }

    // Responsibility 2: format the report
    public String formatAsHtml(List<Patient> patients) {
        // HTML generation here
    }

    // Responsibility 3: send by email
    public void sendEmail(String recipient, String content) {
        // SMTP code here
    }
}

This class changes for three reasons: when the query changes, when the HTML format changes, and when the email server configuration changes.

Fixed

public class PatientRepository {
    public List<Patient> getAdmittedPatients() { ... }
}

public class PatientReportFormatter {
    public String formatAsHtml(List<Patient> patients) { ... }
}

public class EmailService {
    public void send(String recipient, String content) { ... }
}

Each class now has exactly one reason to change.


O — Open-Closed Principle (OCP)

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

You should be able to add new behaviour without modifying existing, tested code. This is achieved by depending on abstractions (interfaces or abstract classes) so new implementations can be introduced without touching existing ones.

Violation

public class DiscountCalculator {
    public double calculate(String customerType, double amount) {
        if (customerType.equals("STAFF")) return amount * 0.8;
        if (customerType.equals("PENSIONER")) return amount * 0.85;
        // Adding "STUDENT" requires modifying this class
        return amount;
    }
}

Every new discount type requires editing this class, risking regressions in the existing logic.

Fixed

public interface DiscountPolicy {
    boolean appliesTo(Customer customer);
    double apply(double amount);
}

public class StaffDiscount implements DiscountPolicy {
    public boolean appliesTo(Customer c) { return c.isStaff(); }
    public double apply(double amount) { return amount * 0.8; }
}

public class PensionerDiscount implements DiscountPolicy {
    public boolean appliesTo(Customer c) { return c.isPensioner(); }
    public double apply(double amount) { return amount * 0.85; }
}

public class DiscountCalculator {
    private List<DiscountPolicy> policies;

    public double calculate(Customer customer, double amount) {
        return policies.stream()
            .filter(p -> p.appliesTo(customer))
            .mapToDouble(p -> p.apply(amount))
            .findFirst()
            .orElse(amount);
    }
}

Adding a student discount now means writing a new StudentDiscount class — the DiscountCalculator is never touched.


L — Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types without altering the correctness of the program.

Named after Barbara Liskov, this principle ensures that inheritance is used correctly. If class B extends class A, you should be able to use B anywhere A is expected and the program must still work correctly.

Violation

public class Rectangle {
    protected int width, height;
    public void setWidth(int w) { this.width = w; }
    public void setHeight(int h) { this.height = h; }
    public int area() { return width * height; }
}

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

// This behaves incorrectly when given a Square
void resize(Rectangle r) {
    r.setWidth(5);
    r.setHeight(10);
    assert r.area() == 50; // Fails for Square — area is 100
}

Square violates LSP because it changes the postconditions of setWidth and setHeight.

The fix

Recognise that a square is not a rectangle in the behavioural sense needed here. Either do not use inheritance, or redesign the hierarchy around an immutable Shape abstraction.

Practical test for LSP

Ask: "If I replace every instance of the base class with this subclass, will anything break?" If the answer is yes, the subclass violates LSP.


I — Interface Segregation Principle (ISP)

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

A large, general-purpose interface forces every implementer to provide methods it may not need, leading to empty or throwing implementations. Prefer many small, focused interfaces.

Violation

public interface StaffMember {
    void prescribeMedication();   // only doctors do this
    void dispenseMedication();    // only pharmacists do this
    void administerInjection();   // only nurses do this
    void processPayment();        // only cashiers do this
    String getName();
}

A cashier implementing this interface must stub out prescribeMedication() and dispenseMedication().

Fixed

public interface Prescriber {
    void prescribeMedication();
}

public interface Dispenser {
    void dispenseMedication();
}

public interface Cashier {
    void processPayment();
}

public interface StaffIdentity {
    String getName();
}

// Roles compose the interfaces they actually need
public class Doctor implements Prescriber, StaffIdentity { ... }
public class Pharmacist implements Dispenser, StaffIdentity { ... }
public class CashierStaff implements Cashier, StaffIdentity { ... }

D — Dependency Inversion Principle (DIP)

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

Without DIP, a business-logic class directly references a database class. The business logic becomes impossible to test without the database, and changing the database technology forces changes to the business logic.

Violation

public class BillingService {
    // Direct dependency on a concrete class
    private MySqlBillingRepository repository = new MySqlBillingRepository();

    public Bill createBill(Patient patient, double amount) {
        Bill bill = new Bill(patient, amount);
        repository.save(bill);
        return bill;
    }
}

BillingService is tightly bound to MySQL. It cannot be unit-tested without a real database connection.

Fixed

// Abstraction — both sides depend on this
public interface BillingRepository {
    void save(Bill bill);
    Optional<Bill> findById(long id);
}

// High-level module depends on the abstraction
public class BillingService {
    private BillingRepository repository;

    public BillingService(BillingRepository repository) {
        this.repository = repository;
    }

    public Bill createBill(Patient patient, double amount) {
        Bill bill = new Bill(patient, amount);
        repository.save(bill);
        return bill;
    }
}

// Low-level module implements the abstraction
public class JpaBillingRepository implements BillingRepository {
    @PersistenceContext EntityManager em;

    @Override
    public void save(Bill bill) { em.persist(bill); }
}

// In tests — inject a simple in-memory implementation
public class InMemoryBillingRepository implements BillingRepository {
    private List<Bill> store = new ArrayList<>();
    @Override public void save(Bill bill) { store.add(bill); }
}

In Java EE, CDI handles this injection automatically:

@Stateless
public class BillingService {
    @Inject
    private BillingRepository repository; // CDI injects the appropriate implementation
}

How the Principles Work Together

The SOLID principles are not independent rules — they reinforce each other:

  • SRP gives each class a clear purpose, making it easier to apply OCP (you know what you are extending).
  • LSP ensures that the abstractions used by OCP and DIP behave consistently.
  • ISP keeps interfaces focused, supporting DIP by preventing dependencies on unnecessary behaviour.
  • DIP is what makes OCP practical: you can only be open for extension if you depend on abstractions.

When Not to Over-Apply SOLID

SOLID is guidance, not dogma. Applied without judgement, it produces:

  • Dozens of one-method interfaces for trivial classes
  • Deep inheritance hierarchies no one can follow
  • Abstraction layers that add complexity without adding flexibility

Apply SOLID where complexity warrants it. A simple utility method does not need an interface and three implementations.


Previous: SE-03: Object-Oriented Programming
Next: SE-05: Design Patterns

Back to Software Engineering Principles

Clone this wiki locally