StackWise
Design PatternsPrinciples

SOLID Principles

The five object-oriented design principles behind every good pattern, with before-and-after Java examples.

beginner 4 min readLesson 2 of 25

SOLID is an acronym coined by Robert C. Martin for five principles that keep object-oriented code easy to change. Design patterns are, in large part, recipes for applying these principles.

Letter Principle One-line summary
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 usable wherever the base type is expected
I Interface Segregation Many small, client-specific interfaces beat one fat one
D Dependency Inversion Depend on abstractions, not concretions

S — Single Responsibility Principle

A class should have one reason to change — one actor or stakeholder whose requirements drive it.

class Invoice {
    double total() { /* business rules */ return 0; }
    String toPdf() { /* rendering */ return ""; }
    void save()    { /* SQL */ }
}
record Invoice(List<LineItem> items) {
    double total() { return items.stream().mapToDouble(LineItem::amount).sum(); }
}
class InvoicePdfRenderer { byte[] render(Invoice i) { /* ... */ return new byte[0]; } }
class InvoiceRepository  { void save(Invoice i) { /* ... */ } }

The finance team changing tax rules, the design team changing the PDF layout and the DBA changing the schema no longer edit the same file.

O — Open/Closed Principle

You should be able to add behaviour without editing existing, tested code. The usual tool is polymorphism — often the Strategy pattern.

OpenClosed.javaJava 21
import java.util.*;

interface Discount {
    double apply(double amount);
}

record PercentOff(double pct) implements Discount {
    public double apply(double amount) { return amount * (1 - pct / 100); }
}

record FlatOff(double value) implements Discount {
    public double apply(double amount) { return Math.max(0, amount - value); }
}

class Checkout {
    // Adding a new discount type never requires changing this class
    double total(double amount, List<Discount> discounts) {
        double result = amount;
        for (Discount d : discounts) result = d.apply(result);
        return result;
    }
}

class Main {
    public static void main(String[] args) {
        var checkout = new Checkout();
        Discount blackFriday = amount -> amount > 100 ? amount - 25 : amount; // a new rule as a lambda
        System.out.println(checkout.total(200, List.of(new PercentOff(10), new FlatOff(5))));
        System.out.println(checkout.total(200, List.of(blackFriday)));
    }
}

L — Liskov Substitution Principle

If S is a subtype of T, code written against T must keep working when given an S — no strengthened preconditions, weakened postconditions or surprise exceptions.

The classic violation is Square extends Rectangle:

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

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

void resize(Rectangle r) {
    r.setWidth(5);
    r.setHeight(4);
    assert r.area() == 20; // fails for Square: area is 16
}

I — Interface Segregation Principle

Clients should not be forced to depend on methods they don’t use.

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

class Robot implements Worker {
    public void work() { /* ... */ }
    public void eat() { throw new UnsupportedOperationException(); } // forced!
    public void attendStandup() { throw new UnsupportedOperationException(); }
}
interface Workable { void work(); }
interface Feedable { void eat(); }

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

D — Dependency Inversion Principle

High-level policy should not depend on low-level details; both should depend on abstractions. This is what makes code testable and what Spring’s dependency injection is built around.

classDiagram
  direction LR
  class OrderService {
    -PaymentGateway gateway
    +placeOrder(Order)
  }
  class PaymentGateway {
    <<interface>>
    +charge(amount) Receipt
  }
  class StripeGateway
  class FakeGateway
  OrderService --> PaymentGateway
  PaymentGateway <|.. StripeGateway
  PaymentGateway <|.. FakeGateway
@Service
class OrderService {
    private final PaymentGateway gateway;           // abstraction

    OrderService(PaymentGateway gateway) {          // injected, not created with `new`
        this.gateway = gateway;
    }
}

In tests you pass a FakeGateway; in production Spring injects StripeGateway. OrderService never changes.

SOLID and patterns

Principle Patterns that apply it
SRP Facade, Command, Visitor
OCP Strategy, Decorator, Observer, Template Method
LSP Every pattern relying on polymorphism
ISP Adapter, role interfaces
DIP Abstract Factory, Factory Method, Dependency Injection
Finished this lesson?

Mark it complete to track your progress through Design Patterns.