A Deep Technical Guide with Real-World Usage, Patterns, and Architectural Context
Target Audience: Beginner → Architect
Depth: Deep technical with code, diagrams, and architectural discussion
Language: Java (JDK 21+ compatible, applicable to modern stacks including Spring Boot, microservices, and AI-driven systems)
We live in a time where:
Yet one thing remains unchanged: bad design does not scale.
AI can generate code. It cannot automatically generate good architecture.
SOLID principles remain foundational because they reduce coupling, improve extensibility, and prevent architectural entropy.
SOLID is not about theory. It is about controlling complexity in evolving systems.
| Principle | Full Name | Core Idea |
|---|---|---|
| S | Single Responsibility Principle | One reason to change |
| O | Open/Closed Principle | Open for extension, closed for modification |
| L | Liskov Substitution Principle | Subtypes must preserve behavior |
| I | Interface Segregation Principle | No client should depend on methods it doesn't use |
| D | Dependency Inversion Principle | Depend on abstractions, not concretions |
A class should have one, and only one, reason to change.
class InvoiceService {
public void calculateTotal(Invoice invoice) {
// business logic
}
public void saveToDatabase(Invoice invoice) {
// persistence logic
}
public void printInvoice(Invoice invoice) {
// presentation logic
}
}
This class has three responsibilities:
If database schema changes → modify class
If printing format changes → modify class
If tax rules change → modify class
This creates tight coupling.
class InvoiceCalculator {
public double calculateTotal(Invoice invoice) {
return invoice.getItems()
.stream()
.mapToDouble(Item::getPrice)
.sum();
}
}
class InvoiceRepository {
public void save(Invoice invoice) {
// DB logic
}
}
class InvoicePrinter {
public void print(Invoice invoice) {
// printing logic
}
}
Now:
Invoice
↓
[InvoiceCalculator]
↓
[InvoiceRepository]
↓
[InvoicePrinter]
Software entities should be open for extension but closed for modification.
class DiscountCalculator {
public double calculate(String type, double amount) {
if (type.equals("REGULAR"))
return amount * 0.9;
if (type.equals("VIP"))
return amount * 0.8;
return amount;
}
}
Each new discount type → modify class.
interface DiscountStrategy {
double apply(double amount);
}
class RegularDiscount implements DiscountStrategy {
public double apply(double amount) {
return amount * 0.9;
}
}
class VipDiscount implements DiscountStrategy {
public double apply(double amount) {
return amount * 0.8;
}
}
class DiscountCalculator {
public double calculate(DiscountStrategy strategy, double amount) {
return strategy.apply(amount);
}
}
Now new discounts can be added without modifying existing code.
classDiagram class DiscountStrategy { <<interface>> +apply(amount) } class RegularDiscount class VipDiscount class DiscountCalculator DiscountStrategy <|.. RegularDiscount DiscountStrategy <|.. VipDiscount DiscountCalculator --> DiscountStrategy
Subtypes must be substitutable for their base types without breaking behavior.
class Bird {
public void fly() {}
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException();
}
}
This breaks substitutability.
interface Bird {}
interface FlyingBird extends Bird {
void fly();
}
class Sparrow implements FlyingBird {
public void fly() {}
}
class Penguin implements Bird {}
Now behavior is preserved.
classDiagram class Bird class FlyingBird class Sparrow class Penguin Bird <|-- FlyingBird Bird <|-- Penguin FlyingBird <|-- Sparrow
Clients should not be forced to depend on methods they do not use.
interface Worker {
void work();
void eat();
}
Robot class:
class Robot implements Worker {
public void work() {}
public void eat() { throw new UnsupportedOperationException(); }
}
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Human implements Workable, Eatable {
public void work() {}
public void eat() {}
}
class Robot implements Workable {
public void work() {}
}
High-level modules should not depend on low-level modules.
Both should depend on abstractions.
class MySQLDatabase {
public void save(String data) {}
}
class UserService {
private MySQLDatabase database = new MySQLDatabase();
public void saveUser(String user) {
database.save(user);
}
}
UserService tightly coupled to MySQL.
interface Database {
void save(String data);
}
class MySQLDatabase implements Database {
public void save(String data) {}
}
class UserService {
private final Database database;
public UserService(Database database) {
this.database = database;
}
public void saveUser(String user) {
database.save(user);
}
}
Now we can inject PostgreSQL, MongoDB, or even a mock.
[UserService]
↓ depends on
[Database Interface]
↑
[MySQL] [Postgres] [Mock]
AI often generates:
Developers must refactor AI output into SOLID-compliant structures.
| Principle | Supported By | Violated By |
|---|---|---|
| SRP | Repository, MVC | God Object |
| OCP | Strategy, Decorator | switch-case logic |
| LSP | Composition | Bad inheritance |
| ISP | Role interfaces | Fat contracts |
| DIP | DI frameworks | Direct instantiation |
SOLID is not about memorizing five definitions.
It is about:
In modern architectures:
When applied correctly, SOLID does not increase complexity — it reduces accidental complexity.
The real skill is knowing when to apply it pragmatically.
Architecture is balance, not dogma.