Definition
- SOLID is an acronym for five object-oriented design principles that lead to code that is easy to extend, test, and maintain.
- S – SRP – a class should have one reason to change; split concerns into focused classes.
- O – OCP – software should be open for extension but closed for modification; add new classes, do not edit old ones.
- L – LSP – a subtype must be usable anywhere its supertype is expected without breaking correctness.
- I – ISP – clients should not depend on methods they do not use; keep interfaces small and role-based.
- D – DIP – high-level modules should depend on abstractions, not concrete implementations; inject dependencies.
At the National Bank of India each employee has one clearly defined job, which is Single Responsibility: the teller does not also audit and send SMS. When the bank adds a new loan product, it opens a new counter rather than rewriting the existing one, which is Open/Closed. A trainee manager must be able to stand in for any manager without confusing customers, which is Liskov. Employees only get the forms relevant to their role, not every form in the bank, which is Interface Segregation. The branch depends on a general payment processor role rather than one named vendor, so it can swap vendors easily, which is Dependency Inversion.
Understanding the Concepts
What is the Single Responsibility Principle?
SRP says a class should change for exactly one reason. A class that validates a transfer, computes fees, saves to the database, and sends notifications changes for four different reasons. A change to the SMS provider risks breaking the validation logic.
package com.bank.service;
import java.math.BigDecimal;
// VIOLATION: four reasons to change in one class
public class TransferManagerBad {
public void execute(String from, String to, BigDecimal amount) {
// 1. Validate accounts
if (from == null || to == null) throw new IllegalArgumentException("Invalid accounts");
// 2. Compute fee
BigDecimal fee = amount.multiply(new BigDecimal("0.001"));
// 3. Persist to database
System.out.println("Saving transfer: " + amount + " fee: " + fee);
// 4. Send SMS notification
System.out.println("SMS to " + to + ": You received " + amount);
}
}
The fix splits into focused classes:
package com.bank.service;
import java.math.BigDecimal;
public class TransferValidator {
public void validate(String from, String to) {
if (from == null || to == null) throw new IllegalArgumentException("Invalid accounts");
}
}
public class FeeCalculator {
public BigDecimal compute(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.001"));
}
}
public class TransferRepository {
public void save(String from, String to, BigDecimal amount, BigDecimal fee) {
System.out.println("Saved transfer: " + amount + " fee: " + fee);
}
}
public class NotificationService {
public void notify(String to, BigDecimal amount) {
System.out.println("SMS to " + to + ": You received " + amount);
}
}
Each class now has one reason to change.
What is the Open/Closed Principle?
OCP says you add new behaviour by adding new code, not by editing tested code. A fee calculator with a growing if-else on trade type must be edited every time a new type appears, risking existing branches.
package com.bank.service;
import java.math.BigDecimal;
// VIOLATION: every new trade type edits this method
public class TradeFeeCalculatorBad {
public BigDecimal calculate(String type, BigDecimal amount) {
if ("EQUITY".equals(type)) return amount.multiply(new BigDecimal("0.001"));
else if ("FX".equals(type)) return amount.multiply(new BigDecimal("0.0005"));
// adding DERIVATIVE means editing here
return BigDecimal.ZERO;
}
}
// FIX: strategy interface
public interface FeeStrategy {
BigDecimal calculate(BigDecimal amount);
}
public class EquityFee implements FeeStrategy {
public BigDecimal calculate(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.001"));
}
}
public class FxFee implements FeeStrategy {
public BigDecimal calculate(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.0005"));
}
}
// Adding DerivativeFee is a new class – no edits to existing code
public class DerivativeFee implements FeeStrategy {
public BigDecimal calculate(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.002"));
}
}
What is the Liskov Substitution Principle?
LSP says a subtype must honour the contract of its supertype. The classic violation is Square extends Rectangle: a Rectangle lets you set width and height independently, but Square overrides the setters to keep them equal.
package com.bank.model;
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; }
}
// VIOLATION: overriding setters breaks the Rectangle contract
public class Square extends Rectangle {
@Override
public void setWidth(int w) { this.width = w; this.height = w; }
@Override
public void setHeight(int h) { this.width = h; this.height = h; }
}
// Caller expects Rectangle behaviour:
// rect.setWidth(5); rect.setHeight(10); assert rect.area() == 50;
// But Square gives area = 100!
The fix is to not model Square as a subtype of Rectangle. Use a common Shape abstraction with an area() method instead.
public interface Shape {
int area();
}
public class RectangleFixed implements Shape {
private final int width, height;
public RectangleFixed(int w, int h) { this.width = w; this.height = h; }
public int area() { return width * height; }
}
public class SquareFixed implements Shape {
private final int side;
public SquareFixed(int s) { this.side = s; }
public int area() { return side * side; }
}
What is the Interface Segregation Principle?
ISP says do not force a class to implement methods it does not use. A fat Animal interface forces Dog to throw on fly().
package com.bank.model;
// VIOLATION: fat interface
public interface Animal {
void eat();
void sleep();
void fly();
void swim();
}
public class Dog implements Animal {
public void eat() { System.out.println("Dog eats"); }
public void sleep() { System.out.println("Dog sleeps"); }
public void fly() { throw new UnsupportedOperationException("Dogs cannot fly"); }
public void swim() { System.out.println("Dog swims"); }
}
// FIX: small role interfaces
public interface Eater { void eat(); }
public interface Swimmer { void swim(); }
public interface Flyer { void fly(); }
public class DogFixed implements Eater, Swimmer {
public void eat() { System.out.println("Dog eats"); }
public void swim() { System.out.println("Dog swims"); }
}
In banking, a fat Account interface with calculateInterest, checkOverdraft, and tradeStocks forces a plain savings account to stub out trading. Split into InterestBearing, Overdraftable, and Tradable.
What is the Dependency Inversion Principle?
DIP says high-level policy should depend on an abstraction, and the concrete detail should be injected. A TransferService that does new EmailNotifier() inside itself is welded to email and is hard to test.
package com.bank.service;
import java.math.BigDecimal;
// VIOLATION: welded to EmailNotifier
public class TransferServiceBad {
private final EmailNotifier notifier = new EmailNotifier();
public void transfer(String from, String to, BigDecimal amount) {
System.out.println("Transferred " + amount);
notifier.send(to, "You received " + amount);
}
}
// FIX: depend on abstraction, inject implementation
public interface Notifier {
void send(String to, String message);
}
public class EmailNotifier implements Notifier {
public void send(String to, String message) {
System.out.println("Email to " + to + ": " + message);
}
}
public class SmsNotifier implements Notifier {
public void send(String to, String message) {
System.out.println("SMS to " + to + ": " + message);
}
}
public class TransferServiceFixed {
private final Notifier notifier;
public TransferServiceFixed(Notifier notifier) {
this.notifier = notifier;
}
public void transfer(String from, String to, BigDecimal amount) {
System.out.println("Transferred " + amount);
notifier.send(to, "You received " + amount);
}
}
Now you can inject a mock in tests or an SMS notifier in production without touching the service.
How do SOLID principles reinforce each other?
These principles work as a system. DIP plus OCP is what makes the Strategy pattern powerful: you depend on the strategy interface (DIP) and add new strategies without editing (OCP). ISP makes LSP easier because small contracts are easier to honour fully.
package com.bank.demo;
import java.math.BigDecimal;
import java.util.Map;
public class SolidTogetherDemo {
public static void main(String[] args) {
// OCP + DIP: strategies selected at runtime
Map<String, FeeStrategy> fees = Map.of(
"EQUITY", new EquityFee(),
"FX", new FxFee()
);
String tradeType = "EQUITY";
BigDecimal amount = new BigDecimal("500000");
BigDecimal fee = fees.get(tradeType).calculate(amount);
System.out.println(tradeType + " fee: " + fee);
// DIP: inject notifier
TransferServiceFixed svc = new TransferServiceFixed(new SmsNotifier());
svc.transfer("ACC-001", "ACC-002", new BigDecimal("25000"));
}
}
EQUITY fee: 500.000
Transferred 25000
SMS to ACC-002: You received 25000
What are common mistakes when applying SOLID?
- Applying SRP too aggressively and creating dozens of one-line classes that are harder to navigate than the original.
- Creating interfaces for classes that will only ever have one implementation, adding indirection for no benefit.
- Confusing ISP with "one method per interface" – ISP is about role cohesion, not minimum size.
- Using DIP where no variation or testing seam is needed, creating pointless abstraction layers.
What should a senior engineer know?
- SOLID principles are guidance, not rigid rules. Applied mechanically they create needless interfaces.
- The real skill is recognising when a principle earns its place: a second implementation, a growing switch, a hard-to-test dependency.
- In Spring, DIP is automatic through constructor injection, and OCP is natural with bean registries.
- In a microservice, SRP maps to bounded contexts: each service has one business reason to exist.
Visual Overview
SOLID Violation → Fix Flow
──────────────────────────
SRP Violation: SRP Fix:
┌──────────────────────┐ ┌─────────────┐ ┌──────────────┐
│ TransferManager │ │ Validator │ │ FeeCalculator│
│ - validate │ ──▶ ├─────────────┤ ├──────────────┤
│ - compute fee │ │ Repository │ │ Notifier │
│ - persist │ └─────────────┘ └──────────────┘
│ - send notification │
└──────────────────────┘
OCP Violation: OCP Fix:
┌──────────────────────┐ ┌──────────────┐
│ if EQUITY ... │ │ FeeStrategy │ ◄── interface
│ else if FX ... │ ──▶ └──────┬───────┘
│ else if DERIV ... │ ├── EquityFee
└──────────────────────┘ ├── FxFee
└── DerivativeFee (new)
DIP Violation: DIP Fix:
┌──────────────────────┐ ┌──────────────┐
│ TransferService │ │TransferService│
│ new EmailNotifier() │ ──▶ │ │ │
└──────────────────────┘ │ ▼ │
│ Notifier │ ◄── interface
│ ├─ Email │
│ └─ SMS │
└──────────────┘
Key Takeaway
SOLID is five object-oriented design principles that together produce code that is easy to extend, test, and maintain. Rajesh explains each with the violation it prevents and the fix.
Single Responsibility means a class should have one reason to change. The violation is a TransferManager at National Bank of India that validates, computes fees, persists, and notifies, so a change to the SMS vendor risks breaking validation. The fix is to split it into a TransferValidator, a FeeCalculator, a TransferRepository, and a NotificationService. Open/Closed means software should be open for extension but closed for modification. The violation is a growing if-else on trade type inside a fee method. The fix is a FeeStrategy interface with one implementation per type, so a new type is a new class with no edits to existing code.
Liskov Substitution means a subtype must be usable anywhere its supertype is expected. The classic violation is Square extending Rectangle, where overriding the setters breaks any caller that sets width and height independently. The fix is a common Shape abstraction. Interface Segregation means clients should not depend on methods they do not use. The violation is a fat Animal interface that forces Dog to throw on fly(). The fix is small role interfaces like Eater, Swimmer, and Flyer. In banking, Pankaj splits a fat account interface into InterestBearing and Tradable so a plain savings account is not forced to stub out trading.
Dependency Inversion means high-level modules depend on abstractions, not concrete classes, and the implementation is injected. The violation is a TransferService that does new EmailNotifier() inside itself. The fix is to depend on a Notifier interface and inject the implementation through the constructor, so tests can inject a mock and production can swap SMS for email.
Remember one sentence: SOLID gives five focused rules: split by reason to change, extend by adding classes, honour supertype contracts, keep interfaces small, and depend on injected abstractions.
---