jongkwan.dev
Development · Essay №002

The SOLID Principles

Definitions of SRP, OCP, LSP, ISP, and DIP with violation and application examples, and how they relate to architectural patterns

Jongkwan Lee2025년 1월 18일10 min read
Contents

SOLID is a set of five object-oriented design principles: single responsibility (SRP), open-closed (OCP), Liskov substitution (LSP), interface segregation (ISP), and dependency inversion (DIP).

Overview

SOLID is an acronym for the five object-oriented design principles formulated by Robert C. Martin (Uncle Bob). The name is built from the initials of single responsibility (SRP), open-closed (OCP), Liskov substitution (LSP), interface segregation (ISP), and dependency inversion (DIP). Their purpose is to make software easier to understand, more flexible, and easier to maintain. They were formulated in the early 2000s and, more than twenty years later, remain widely used in microservice and cloud-native environments.

Single responsibility principle (SRP)

Definition

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

A class must have exactly one reason to change. In other words, one class handles one responsibility.

Violation

java
// SRP violation: the User class handles business logic, serialization, and DB access
public class User {
    public void calculateSalary() { /* salary calculation */ }
    public String toJson() { /* JSON serialization */ }
    public void saveToDatabase() { /* DB persistence */ }
}

Correct application

java
public class User { /* user domain logic only */ }
public class SalaryCalculator { /* responsible for salary calculation */ }
public class UserSerializer { /* responsible for serialization */ }
public class UserRepository { /* responsible for data access */ }

SRP and microservices

In a microservice architecture, SRP becomes the base principle for drawing service boundaries:

  • One service = one business domain
  • A service holding several responsibilities → consider splitting it
  • Directly connected to the bounded context of domain-driven design (DDD)

Open-closed principle (OCP)

Definition

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

Software entities (classes, modules, functions) must be open for extension and closed for modification.

The core idea

New behavior must be addable without changing existing code. Abstraction and polymorphism are the tools for that.

Violation

java
// OCP violation: adding a new payment method requires editing existing code
public class PaymentProcessor {
    public void process(String type) {
        if (type.equals("card")) { /* card payment */ }
        else if (type.equals("bank")) { /* bank transfer */ }
        // every new payment method adds another else if -> modification required
    }
}

Correct application

java
// OCP satisfied: a new payment method is added by implementing the interface
public interface PaymentStrategy {
    void process(PaymentRequest request);
}
 
public class CardPayment implements PaymentStrategy { /* card */ }
public class BankTransfer implements PaymentStrategy { /* bank transfer */ }
public class CryptoPayment implements PaymentStrategy { /* crypto -- extension */ }
 
public class PaymentProcessor {
    private final PaymentStrategy strategy;
 
    public PaymentProcessor(PaymentStrategy strategy) {
        this.strategy = strategy;
    }
 
    public void process(PaymentRequest request) {
        strategy.process(request); // no change to existing code
    }
}

OCP and plugin architecture

The fullest realization of OCP is the plugin architecture:

  • Extend functionality through plugins without changing the core system
  • IDEs (IntelliJ, VS Code) and build tools (Gradle, Webpack) are the familiar examples
  • Routing rules in a microservice API gateway are another application of OCP

Liskov substitution principle (LSP)

Definition

"Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program."

Replacing an object of a supertype with an object of a subtype must preserve the correctness of the program. Barbara Liskov proposed it in a 1987 OOPSLA keynote, and it was formalized in a 1994 paper by Liskov and Jeannette Wing.

The classic violation: the rectangle-square problem

java
public class Rectangle {
    protected int width;
    protected int height;
 
    public void setWidth(int w) { this.width = w; }
    public void setHeight(int h) { this.height = h; }
    public int area() { return width * height; }
}
 
// LSP violation: a square must always keep width and height equal,
// which breaks the contract of Rectangle
public class Square extends Rectangle {
    @Override
    public void setWidth(int w) {
        this.width = w;
        this.height = w; // side effect the caller does not expect
    }
}

Correct design

java
public interface Shape {
    int area();
}
 
public class Rectangle implements Shape {
    private final int width;
    private final int height;
    public int area() { return width * height; }
}
 
public class Square implements Shape {
    private final int side;
    public int area() { return side * side; }
}

LSP and design by contract

LSP is closely tied to Bertrand Meyer's design by contract:

  • Precondition: a subtype may require a weaker precondition than the supertype
  • Postcondition: a subtype must guarantee a stronger postcondition than the supertype
  • Invariant: a subtype must preserve the invariants of the supertype

Interface segregation principle (ISP)

Definition

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

A client must not be forced to depend on methods it does not use.

Violation

java
// ISP violation: every worker has to implement eat()
public interface Worker {
    void work();
    void eat();
    void sleep();
}
 
// a robot worker has no need for eat() or sleep()
public class RobotWorker implements Worker {
    public void work() { /* perform work */ }
    public void eat() { /* forced, pointless implementation */ }
    public void sleep() { /* forced, pointless implementation */ }
}

Correct application

java
public interface Workable { void work(); }
public interface Feedable { void eat(); }
public interface Sleepable { void sleep(); }
 
public class HumanWorker implements Workable, Feedable, Sleepable {
    public void work() { /* work */ }
    public void eat() { /* eat */ }
    public void sleep() { /* sleep */ }
}
 
public class RobotWorker implements Workable {
    public void work() { /* work only */ }
}

ISP and microservice API design

ISP applies directly to microservice API design:

  • BFF pattern (backend for frontend): expose only the APIs each client needs
  • GraphQL: the client requests only the fields it needs -- the same spirit as ISP
  • API versioning: keep legacy clients from depending on new fields

Dependency inversion principle (DIP)

Definition

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

High-level modules must not depend on low-level modules; both must depend on abstractions.

Violation

java
// DIP violation: the high-level module depends directly on concrete implementations
public class OrderService {
    private MySQLDatabase database = new MySQLDatabase(); // concrete dependency
    private SmtpMailer mailer = new SmtpMailer();          // concrete dependency
}

Correct application

java
// DIP satisfied: depend on abstractions
public class OrderService {
    private final Database database;       // interface dependency
    private final MailService mailer;      // interface dependency
 
    public OrderService(Database database, MailService mailer) {
        this.database = database;          // injected by the DI container
        this.mailer = mailer;
    }
}

DI containers and IoC

The mechanism that realizes DIP is dependency injection (DI):

DI styleDescriptionExample
Constructor injectionDependencies passed through the constructorSpring's @Autowired
Setter injectionDependencies passed through setter methodsSuited to optional dependencies
Interface injectionDependencies passed through a dedicated interfaceLess common

IoC container (inversion of control container): the DI containers in Spring, NestJS, and .NET manage object lifecycles and dependency resolution automatically.

SOLID and architectural patterns

Clean architecture

Clean architecture, proposed by Robert C. Martin, extends the SOLID principles to the architectural level:

The dependency rule: dependencies always point inward, from outside to inside → a direct application of DIP

Hexagonal architecture (ports and adapters)

Hexagonal architecture, proposed by Alistair Cockburn, realizes DIP through ports and adapters:

  • Driving port (primary): the interface through which the outside world calls the application (API, CLI)
  • Driven port (secondary): the interface through which the application calls external infrastructure (DB, messaging)
  • Adapter: the concrete implementation of a port

Netflix applied hexagonal architecture to separate microservice domain logic completely from infrastructure.

CQRS (command query responsibility segregation)

CQRS separates the responsibilities of reads (queries) and writes (commands):

  • The result of applying SRP to the data access layer
  • The read model and write model can be optimized independently
  • Combined with event sourcing, it yields a complete audit trail
  • Connects to the event sourcing pattern among behavioral patterns

Tactical patterns in domain-driven design (DDD)

The tactical patterns of DDD are SOLID applied to domain modeling:

DDD patternRelated SOLID principleDescription
EntitySRPDomain object with a unique identifier
Value objectSRP, immutabilityObject compared by value rather than identity
AggregateSRP, ISPCluster of entities inside a consistency boundary
RepositoryDIP, ISPInterface that abstracts data access
Domain serviceSRPDomain logic that belongs to no entity
Application serviceSRP, OCPUse case coordination
Domain eventOCPNotification of a domain state change

Modern criticism and evolution

Criticism

The criticisms of SOLID raised most often in practice are the following.

  1. Excessive abstraction: applying SOLID blindly explodes the number of unnecessary interfaces and classes
  2. Applicability in functional programming: OCP and LSP take a different shape in FP
  3. YAGNI (you ain't gonna need it): over-designing for extensibility that is not needed yet

A pragmatic guide

  • Always apply: SRP, DIP -- the most direct effect on code quality
  • Apply as appropriate: OCP, ISP -- focus on areas that change often
  • Apply carefully: LSP -- prefer composition over inheritance

Summary

SOLID comprises five principles: SRP, OCP, LSP, ISP, and DIP. They cover everything from separating class responsibilities to controlling the direction of dependencies between modules. Clean architecture, hexagonal architecture, and DDD tactical patterns are examples of extending those principles to the architectural level.

In practice, apply SRP and DIP first, focus OCP and ISP on areas that change often, and reduce LSP violations by choosing composition over inheritance. Applying the principles blindly produces too much abstraction, so weigh them against YAGNI. The next article covers how these principles take concrete form as design patterns.

References

  • Robert C. Martin, Clean Architecture (2017)
  • Robert C. Martin, "The Principles of OOD" (butunclebob.com)
  • Alistair Cockburn, "Hexagonal Architecture" (2005)
  • Eric Evans, Domain-Driven Design (2003)
  • Netflix Technology Blog, "Ready for changes with Hexagonal Architecture"