Mastering SOLID Principles in Java: Code Examples, Diagrams & Interview Insights
Write maintainable, scalable, and testable enterprise Java with SRP, OCP, LSP, ISP, and DIP — backed by real-world banking/finance examples, refactorings, and accessible diagrams.
Introduction
SOLID is an acronym popularized by Robert C. Martin for five OO design principles. In enterprise Java, these guardrails reduce coupling, protect against regressions, and make code easier to extend and test. They also appear frequently in interview discussions, pairing well with topics like design patterns, clean architecture, and unit testing.
1) Single Responsibility Principle (SRP)
Definition: A class should have one and only one reason to change — it should own a single responsibility.
Common Violation (Banking example)
class BankService {
public void deposit(String account, double amount) {
// deposit logic
}
public void withdraw(String account, double amount) {
// withdraw logic
}
public void printStatement(String account) {
// generate and print statement
}
public void sendEmail(String account, String message) {
// email logic
}
}
Refactored (Separation of responsibilities)
class TransactionService {
public void deposit(String account, double amount) { /* logic */ }
public void withdraw(String account, double amount) { /* logic */ }
}
class StatementService {
public void printStatement(String account) { /* logic */ }
}
class NotificationService {
public void sendEmail(String account, String message) { /* logic */ }
}
SRP Diagram (textual UML)
Poor: BankService → { deposit, withdraw, printStatement, sendEmail }
Better:
TransactionService → { deposit, withdraw }
StatementService → { printStatement }
NotificationService→ { sendEmail }
Impact: Higher cohesion, simpler unit tests, and fewer unintended side-effects when a requirement changes.
2) Open/Closed Principle (OCP)
Definition: Software entities should be open for extension but closed for modification.
Common Violation (Loan approval branching)
class LoanApproval {
public void approveLoan(String type) {
if ("Home".equals(type)) {
// home loan logic
} else if ("Car".equals(type)) {
// car loan logic
} else if ("Personal".equals(type)) {
// personal loan logic
}
}
}
Refactored (Polymorphic extension)
interface Loan { void approve(); }
class HomeLoan implements Loan {
public void approve() { /* home loan logic */ }
}
class CarLoan implements Loan {
public void approve() { /* car loan logic */ }
}
class PersonalLoan implements Loan {
public void approve() { /* personal loan logic */ }
}
class LoanApproval {
public void process(Loan loan) { loan.approve(); }
}
OCP Diagram (textual UML)
Poor: LoanApproval → if/else chains for Home, Car, Personal...
Better:
Loan (interface) ↑ implements HomeLoan CarLoan PersonalLoan LoanApproval → process(Loan)
Impact: You add new variants by adding new classes — existing, tested code remains untouched and stable.
3) Liskov Substitution Principle (LSP)
Definition: Subtypes must be substitutable for their base types without breaking expected behavior.
Common Violation (Fixed deposit withdrawal)
UnsupportedOperationException for a method promised by the base type.class Account {
public void deposit(double amount) { /* logic */ }
public void withdraw(double amount) { /* logic */ }
}
class FixedDepositAccount extends Account {
@Override
public void withdraw(double amount) {
throw new UnsupportedOperationException("Withdrawal not allowed");
}
}
Refactored (Refine the abstraction)
interface Account { void deposit(double amount); }
interface WithdrawableAccount extends Account { void withdraw(double amount); }
class SavingsAccount implements WithdrawableAccount {
public void deposit(double amount) { /* logic */ }
public void withdraw(double amount) { /* logic */ }
}
class FixedDepositAccount implements Account {
public void deposit(double amount) { /* logic */ }
}
LSP Diagram (textual UML)
Poor: Account {deposit, withdraw} ← FixedDepositAccount overrides withdraw but breaks contract.
Better:
Account { deposit }
WithdrawableAccount extends Account { withdraw }
SavingsAccount implements WithdrawableAccount
FixedDepositAccount implements Account
Impact: Predictable polymorphism, fewer runtime surprises, and clearer contracts for clients.
4) Interface Segregation Principle (ISP)
Definition: Clients should not be forced to depend on interfaces they do not use.
Common Violation (Overly fat payment interface)
interface Payment {
void pay();
void refund();
void cashBack();
}
class UpiPayment implements Payment {
public void pay() { /* logic */ }
public void refund() { /* logic */ }
public void cashBack() { /* not applicable */ }
}
Refactored (Focused, role-based interfaces)
interface Payment { void pay(); }
interface Refundable { void refund(); }
interface Cashbackable { void cashBack(); }
class UpiPayment implements Payment, Refundable {
public void pay() { /* logic */ }
public void refund() { /* logic */ }
}
ISP Diagram (textual UML)
Poor: Payment {pay, refund, cashBack} ← UpiPayment implements but cashBack is irrelevant.
Better:
Payment { pay } Refundable { refund } Cashbackable { cashBack }
UPIPayment implements Payment, Refundable
Impact: Smaller, purpose-built interfaces reduce coupling and accidental implementation debt.
5) Dependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.
Common Violation (Tight DB coupling)
class MySQLDatabase {
public void save(String data) { /* logic */ }
}
class BankingApp {
private final MySQLDatabase database = new MySQLDatabase();
public void store(String data) { database.save(data); }
}
Refactored (Depend on abstractions)
interface Database { void save(String data); }
class MySQLDatabase implements Database {
public void save(String data) { /* logic */ }
}
class OracleDatabase implements Database {
public void save(String data) { /* logic */ }
}
class BankingApp {
private final Database database;
public BankingApp(Database database) { this.database = database; }
public void store(String data) { database.save(data); }
}
DIP Diagram (textual UML)
Poor: BankingApp → MySQLDatabase (concrete → concrete)
Better:
Database (interface) ↑ implemented by MySQLDatabase, OracleDatabase BankingApp → Database (constructor-injected)
How SOLID helps in interviews & real projects
- Maintainability: Smaller, cohesive classes = easier reasoning & change management.
- Scalability: Add features via new classes instead of editing core logic.
- Testability: Clear seams for mocking and isolating units.
- Architecture maturity: Demonstrates design literacy in technical interviews.
Practical tips for enterprise projects & interviews
- Refactor iteratively: Tackle one smell at a time; write safety nets with unit tests.
- Abstractions with purpose: Don’t create interfaces “just because”. Tie them to variability points (e.g., payment providers, DBs).
- Sketch first: Draw small diagrams of responsibilities and dependencies before coding.
- Banking analogies: Use accounts, loans, payments to explain principles crisply in interviews.
- Interview pattern: Define → show violation → refactor → trade-offs → testing strategy.
- As a full-stack Java dev: Apply SOLID in services, controllers, repositories, and DTO mappers; pair with SR tests and CI checks.
- Audit a recent service for SRP violations; split responsibilities.
- Replace
if/elsefeature flags with polymorphism (OCP). - Review hierarchies for LSP breaks (no unsupported operations).
- Split fat interfaces by client needs (ISP).
- Introduce interfaces + DI/IoC for external concerns (DIP).
References & further learning
- Robert C. Martin — Clean Architecture
- Joshua Bloch — Effective Java
- Daily Code Buffer — Learn SOLID Principles — Interview Questions (YouTube)
Comments
Post a Comment