SOLID is an acronym for five essential object-oriented design principles that promote maintainable, flexible, and robust software architecture.
Tip: Interactive SOLID Lab
Explore interactive violation-vs-fix comparisons with dynamic Mermaid architecture diagrams in the SOLID Lab.
1. Single Responsibility Principle (SRP)
"A class should have one, and only one, reason to change."
A class should have one main responsibility. When unrelated concerns live in the same class, modifying one responsibility risks inadvertently breaking the others.
Violating SRP
A single manager class handles payroll calculation, email notification, reporting, and database storage:
Each cohesive concern is separated into its own dedicated class:
class SalaryCalculator {
double calculate(Employee e) {
return 0;
}
}
class EmailService {
void send(Employee e, String msg) {
}
}
class ReportGenerator {
void print(List<Employee> list) {
}
}
class EmployeeRepository {
void save(Employee e) {
}
}
Now changing how salaries are calculated cannot accidentally introduce regressions into email dispatch or database persistence.
2. Open-Closed Principle (OCP)
"Software entities should be open for extension, but closed for modification."
You should be able to introduce new behavior without editing existing, already-tested classes.
Violating OCP
Using type-checking instanceof cascades means every newly introduced shape forces modifications to calculateArea:
class AreaCalculator {
double calculateArea(Object shape) {
if (shape instanceof Circle) {
Circle c = (Circle) shape;
return Math.PI * c.radius * c.radius;
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return r.width * r.height;
}
// every new shape requires editing this method!
return 0;
}
}
Following OCP
Invert the flow using polymorphic contracts:
interface Shape {
double area();
}
class Circle implements Shape {
double radius;
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle implements Shape {
double width, height;
public double area() {
return width * height;
}
}
class AreaCalculator {
double calculateArea(Shape shape) {
return shape.area();
}
}
Extending Later with Zero Modifications
Introducing a new Triangle requires zero edits to AreaCalculator:
class Triangle implements Shape {
double base, height;
public double area() {
return 0.5 * base * height;
}
}
3. Liskov Substitution Principle (LSP)
"Subtypes must be substitutable for their base types without altering system correctness."
If code expects an instance of superclass T, it must function correctly when given an instance of subclass S. Subclasses must honor all contracts established by their parent.
Violating LSP
Subclasses that reject inherited methods break caller assumptions:
class Bird {
void fly() {
System.out.println("Flying");
}
}
class Ostrich extends Bird {
void fly() {
throw new UnsupportedOperationException("Ostriches cannot fly");
}
}
// Bird bird = new Ostrich();
// bird.fly(); // crashes unexpectedly at runtime!
Following LSP
Separate shared traits (eat()) from specialized capabilities (fly()):
class Bird {
void eat() {
System.out.println("Eating");
}
}
interface FlyingBird {
void fly();
}
class Sparrow extends Bird implements FlyingBird {
public void fly() {
System.out.println("Flying");
}
}
class Ostrich extends Bird {
// inherits eat(), makes no false fly() promises
}
4. Interface Segregation Principle (ISP)
"Clients should not be forced to depend upon interfaces that they do not use."
Fat interfaces force implementers to write dummy methods or throw exceptions for operations they cannot perform.
Violating ISP
interface Worker {
void work();
void fly();
void swim();
}
class Robot implements Worker {
public void work() { }
public void fly() {
throw new UnsupportedOperationException();
}
public void swim() {
throw new UnsupportedOperationException();
}
}
Following ISP
Decompose the fat interface into focused, role-specific interfaces:
interface Workable {
void work();
}
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Robot implements Workable {
public void work() { }
}
class Duck implements Workable, Flyable, Swimmable {
public void work() { }
public void fly() { }
public void swim() { }
}
5. Dependency Inversion Principle (DIP)
"High-level modules should not depend on low-level modules. Both should depend on abstractions."
Directly instantiating concrete lower-level dependencies inside a higher-level class couples them tightly together.
Violating DIP
NotificationService directly instantiates a concrete EmailSender:
class EmailSender {
void send(String message) {
System.out.println("Email: " + message);
}
}
class NotificationService {
EmailSender sender = new EmailSender();
void notify(String message) {
sender.send(message);
}
}
Following DIP
Depend on an abstraction (MessageSender) injected at runtime: