The SOLID Principles
Definitions of SRP, OCP, LSP, ISP, and DIP with violation and application examples, and how they relate to architectural patterns
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
// 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
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
// 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
// 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
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
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
// 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
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
// 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
// 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 style | Description | Example |
|---|---|---|
| Constructor injection | Dependencies passed through the constructor | Spring's @Autowired |
| Setter injection | Dependencies passed through setter methods | Suited to optional dependencies |
| Interface injection | Dependencies passed through a dedicated interface | Less 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 pattern | Related SOLID principle | Description |
|---|---|---|
| Entity | SRP | Domain object with a unique identifier |
| Value object | SRP, immutability | Object compared by value rather than identity |
| Aggregate | SRP, ISP | Cluster of entities inside a consistency boundary |
| Repository | DIP, ISP | Interface that abstracts data access |
| Domain service | SRP | Domain logic that belongs to no entity |
| Application service | SRP, OCP | Use case coordination |
| Domain event | OCP | Notification of a domain state change |
Modern criticism and evolution
Criticism
The criticisms of SOLID raised most often in practice are the following.
- Excessive abstraction: applying SOLID blindly explodes the number of unnecessary interfaces and classes
- Applicability in functional programming: OCP and LSP take a different shape in FP
- 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"