Signing you in…

SOLID Principles: Deep Dive

SOLID is an acronym for five object-oriented design principles introduced by Robert Martin in the early 2000s. Each principle addresses a specific failure mode of object-oriented design — symptoms you recognise in real codebases: classes that are impossible to extend, modules that break in unexpected places when changed, code that cannot be tested in isolation. SOLID principles are not rules to apply mechanically — they are lenses for identifying design problems and guiding refactoring. This lesson covers each principle precisely, shows the violation before the fix, and explains the architectural implications.

ℹ️SOLID principles are means, not ends. A class that violates SRP but is 50 lines long, has no collaborators, and will never change is fine. A class that follows every principle mechanically but adds three layers of indirection to a trivial operation is worse than the violation. Apply principles where they solve a real problem.
S — Single Responsibility Principle

Martin's formulation: 'A class should have only one reason to change.' The word 'reason' is key — it refers to an actor or stakeholder whose requirements might drive a change, not a syntactic 'one method' rule. A class that formats reports AND saves them to the database has two reasons to change: the reporting team (format changes) and the ops team (database migration). When those changes happen simultaneously, they conflict. SRP says these concerns belong in separate classes.

Click highlighted lines to understand the SRP violation and the fix
java
1
// ── VIOLATION: two reasons to change in one class ───────────────
2
class OrderProcessor {
3
  void process(Order order) {
4
    // business logic
5
    order.setStatus(CONFIRMED);
6
    // persistence — reason #2 to change
7
    db.save(order);
8
    // notification — reason #3 to change
9
    emailClient.send(order.getEmail(), buildTemplate(order));
10
  }
11
}
12
13
// ── FIX: each class has one reason to change ─────────────────────
14
class OrderConfirmationService {
15
  OrderConfirmationService(OrderRepository repo, OrderNotificationService notifier) { ... }
16
  void confirm(Order order) {
17
    order.confirm();          // domain logic only
18
    repo.save(order);         // delegates persistence
19
    notifier.sendConfirmation(order); // delegates notification
20
  }
21
}
22
class OrderRepository      { void save(Order o) { ... } }   // DB only
23
class OrderNotificationService { void sendConfirmation(Order o) { ... } } // email only
O — Open/Closed Principle

Bertrand Meyer's formulation (1988), reframed by Martin: 'Software entities should be open for extension, but closed for modification.' A class is closed for modification when its tested, deployed code does not need to change to accommodate new behavior. It is open for extension when new behavior can be added by writing new code — a new subclass, a new strategy implementation, a new decorator — without touching the existing class. The most common mechanism: depend on abstractions (interfaces) and inject concrete implementations.

Click highlighted lines to see OCP violated and fixed
java
1
// ── VIOLATION: every new payment method requires modifying this class
2
class PaymentProcessor {
3
  void process(Payment p) {
4
    if (p.type() == CREDIT_CARD) chargeCard(p);
5
    else if (p.type() == PAYPAL)  chargePayPal(p);
6
    else if (p.type() == CRYPTO)  chargeCrypto(p);  // added later, modified existing class
7
  }
8
}
9
10
// ── FIX: closed for modification, open for extension ────────────
11
interface PaymentGateway { void charge(Payment p); }
12
class CreditCardGateway  implements PaymentGateway { ... }
13
class PayPalGateway       implements PaymentGateway { ... }
14
class CryptoGateway       implements PaymentGateway { ... }  // new method = new class only
15
16
class PaymentProcessor {
17
  private final Map<PaymentType, PaymentGateway> gateways;
18
  void process(Payment p) {
19
    gateways.get(p.type()).charge(p);  // never changes
20
  }
21
}
L — Liskov Substitution Principle

Barbara Liskov's formulation (1987): 'If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program.' In plain terms: a subclass must honor the contract of its superclass — not just its method signatures, but its behavioral expectations (preconditions, postconditions, invariants). The classic LSP violation is the Square-Rectangle problem: Square extends Rectangle, but overriding setWidth to also set height violates the rectangle's contract that width and height are independent.

Click highlighted lines to understand LSP violation and the correct design
java
1
// ── VIOLATION: Square breaks Rectangle's behavioral contract ────
2
class Rectangle {
3
  protected int width, height;
4
  void setWidth(int w)  { this.width = w; }
5
  void setHeight(int h) { this.height = h; }
6
  int area() { return width * height; }
7
}
8
class Square extends Rectangle {
9
  @Override void setWidth(int w)  { width = height = w; }  // breaks rectangle contract
10
  @Override void setHeight(int h) { width = height = h; }  // breaks rectangle contract
11
}
12
// Client code breaks when Square is substituted for Rectangle:
13
// rect.setWidth(5); rect.setHeight(4); assert rect.area() == 20; // FAILS for Square
14
15
// ── FIX: model the actual relationship — no inheritance ─────────
16
interface Shape { int area(); }
17
record Rectangle(int width, int height) implements Shape {
18
  public int area() { return width * height; }
19
}
20
record Square(int side) implements Shape {
21
  public int area() { return side * side; }
22
}
I — Interface Segregation Principle

Martin's formulation: 'Clients should not be forced to depend on interfaces they do not use.' Fat interfaces — interfaces with many unrelated methods — force implementors to provide stub implementations of methods they do not need, and force clients to import a type that carries capabilities they do not use. ISP says: prefer many small, client-specific interfaces over one large general-purpose interface. The design pressure from ISP naturally produces Role Interfaces — interfaces that describe a role an object plays (Printable, Saveable, Auditable) rather than the full menu of everything an object can do.

Click highlighted lines to understand ISP violation and role interfaces
java
1
// ── VIOLATION: fat interface forces irrelevant method stubs ─────
2
interface Worker {
3
  void work();
4
  void takeBreak();   // irrelevant for Robot
5
  void receivePaycheck(); // irrelevant for Robot
6
}
7
class Robot implements Worker {
8
  public void work() { ... }
9
  public void takeBreak() { /* Robot does not rest — forced stub */ }
10
  public void receivePaycheck() { /* Robot is not paid — forced stub */ }
11
}
12
13
// ── FIX: small role interfaces ───────────────────────────────────
14
interface Workable  { void work(); }
15
interface Breakable { void takeBreak(); }
16
interface Payable   { void receivePaycheck(); }
17
18
class Robot      implements Workable { ... }                        // only what it needs
19
class HumanWorker implements Workable, Breakable, Payable { ... }   // full set
20
21
// Scheduler only needs Workable — does not know about breaks or pay:
22
class ProductionScheduler { void schedule(List<Workable> workers) { ... } }
D — Dependency Inversion Principle

Martin's formulation: '(A) High-level modules should not depend on low-level modules. Both should depend on abstractions. (B) Abstractions should not depend on details. Details should depend on abstractions.' DIP is the principle that underlies Onion Architecture, Hexagonal Architecture, and Clean Architecture — all the architectures covered in the previous chapter. It is also the principle that makes dependency injection meaningful: DI is the mechanism; DIP is the reason.

Click highlighted lines to trace the dependency inversion from violation to fix
java
1
// ── VIOLATION: high-level module depends on low-level detail ────
2
class OrderService {
3
  private final MySqlOrderRepository repo = new MySqlOrderRepository();
4
  void confirm(Long id) {
5
    Order order = repo.findById(id);
6
    order.confirm();
7
    repo.save(order);
8
  }
9
}
10
11
// ── FIX: both depend on the abstraction ──────────────────────────
12
interface OrderRepository { Order findById(Long id); void save(Order o); }
13
14
class OrderService {
15
  private final OrderRepository repo;  // depends on abstraction
16
  OrderService(OrderRepository repo) { this.repo = repo; }  // DI via constructor
17
  void confirm(Long id) { ... }  // unchanged logic
18
}
19
20
// Low-level detail depends on abstraction (not vice versa):
21
class MySqlOrderRepository implements OrderRepository { ... }    // production
22
class InMemoryOrderRepository implements OrderRepository { ... } // tests
SOLID in Practice: The Full Picture
Quick reference: problem each principle solves and the design mechanism it prescribes
PrincipleSymptom it fixesMechanismArchitectural impact
SRPClasses change for multiple unrelated reasons; merge conflicts; scattered testsSplit by actor/reason to changeDrives modular decomposition; enables independent deployment
OCPAdding features requires modifying tested code; growing if/else chainsDepend on abstractions; extend by adding new implementationsDrives plugin architectures, Strategy pattern, extension points
LSPSubclasses that break calling code; UnsupportedOperationException in overridesHonor the behavioral contract, not just the signatureEnsures substitutability in polymorphic hierarchies; drives composition over inheritance
ISPImplementing irrelevant methods; fat interfaces that change too oftenSmall, role-specific interfaces per client needDrives role interfaces, reduces coupling between modules
DIPHigh-level code depends on DB/framework; untestable without infrastructureDefine abstractions in high-level package; inject detailsFoundation of Onion/Hexagonal/Clean Architecture; enables test doubles
Common SOLID Misapplications
Click each card — mechanical application of SOLID causes its own problems
🔪
SRP Taken Too Far
🔮
Premature OCP Abstractions
🌀
DIP Interface Explosion
Key takeaway: SOLID principles address five concrete failure modes of object-oriented design. SRP prevents multi-actor coupling. OCP prevents modification of tested code. LSP prevents behavioral contract violations in hierarchies. ISP prevents fat-interface coupling. DIP prevents high-level code from depending on low-level details. Apply them where the symptom is present — not as a universal template for every class.
ℹ️What's next: SOLID tells you how to assign responsibilities within a class and how classes should depend on each other. GRASP provides a complementary set of patterns specifically focused on the question of which object should be responsible for what. The next lesson covers all nine GRASP patterns with examples.