SOLID Principles
SOLID is a set of five object-oriented design principles that help in writing:
- Clean code
- Maintainable code
- Scalable systems
- Testable and extensible applications
These principles are extremely important for backend engineers, especially when working with Java, Spring Boot, Microservices, and enterprise systems.
SOLID is not about writing more code — it is about writing code that survives change.
S — Single Responsibility Principle (SRP)
A class should have only one reason to change.
A class should do one thing and do it well.
❌ Violation (Without SRP)
public class UserService {
public void createUser(User user) {
// save user to DB
}
public void sendEmail(User user) {
// send email
}
public void generateReport(User user) {
// generate report
}
}
Problems
- Multiple responsibilities
- Hard to test
- Any change affects the whole class
✅ Correct (With SRP)
public class UserService {
public void createUser(User user) {
// save user
}
}
public class EmailService {
public void sendEmail(User user) {
// send email
}
}
public class ReportService {
public void generateReport(User user) {
// generate report
}
}
Benefits
- Easier to maintain
- Clear ownership
- Better testability
🧠 Backend Insight
In Spring Boot, SRP usually maps to:
- Controller → HTTP handling
- Service → Business logic
- Repository → Data access
O — Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
You should add new behavior without changing existing code.
❌ Violation (If-else hell)
public class InterestCalculator {
public double calculate(String accountType, double balance) {
if ("SAVINGS".equals(accountType)) {
return balance * 0.04;
} else if ("CURRENT".equals(accountType)) {
return balance * 0.02;
}
return 0;
}
}
Problem
- Every new account type requires modifying this class
- Risky in production
✅ Correct (Using Polymorphism)
public interface InterestCalculator {
double calculate(double balance);
}
public class SavingsInterestCalculator implements InterestCalculator {
public double calculate(double balance) {
return balance * 0.04;
}
}
public class FixedDepositInterestCalculator implements InterestCalculator {
public double calculate(double balance) {
return balance * 0.06;
}
}
Benefits
- Add new account types safely
- No existing code touched
- Production-friendly
🧠 Backend Insight
Spring uses OCP heavily:
- JpaRepository
- @Profile
- @AutoConfiguration
- ControllerAdvice
L — Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without breaking behavior.
If class B extends class A, then B should behave like A.
❌ Violation Example
public class Account {
public void withdraw(double amount) {
// withdraw logic
}
}
public class FixedDepositAccount extends Account {
@Override
public void withdraw(double amount) {
throw new UnsupportedOperationException("Withdraw not allowed");
}
}
Problem
- Code expecting Account may crash
- Breaks substitution
✅ Correct Design
public interface Withdrawable {
void withdraw(double amount);
}
public class SavingsAccount implements Withdrawable {
public void withdraw(double amount) {
// allowed
}
}
public class FixedDepositAccount {
// no withdraw method
}
Benefits
- No unexpected behavior
- Clear contracts
- Safer polymorphism
🧠 Rule of Thumb
If you need to throw UnsupportedOperationException, your design is wrong.
I — Interface Segregation Principle (ISP)
Clients should not be forced to depend on methods they do not use.
Prefer many small interfaces over one large interface.
❌ Violation
public interface AccountOperations {
void deposit();
void withdraw();
void calculateInterest();
}
public class FixedDepositAccount implements AccountOperations {
public void withdraw() {
// not applicable
}
}
✅ Correct (Segregated Interfaces)
public interface Depositable {
void deposit();
}
public interface Withdrawable {
void withdraw();
}
public interface InterestBearing {
void calculateInterest();
}
public class FixedDepositAccount implements Depositable, InterestBearing {
// only required methods
}
🧠 Backend Insight
ISP improves:
- API clarity
- Microservice contracts
- Feign clients
- Kafka event consumers
D — Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
❌ Violation (Tight Coupling)
public class AccountService {
private EmailNotificationService emailService = new EmailNotificationService();
}
Problems
- Hard to switch notification type
- Difficult to test
✅ Correct (Using Abstraction)
public interface NotificationService {
void notifyUser(String message);
}
public class EmailNotificationService implements NotificationService {
public void notifyUser(String message) {
// email logic
}
}
public class AccountService {
private final NotificationService notificationService;
public AccountService(NotificationService notificationService) {
this.notificationService = notificationService;
}
}
🧠 Spring Boot Connection
Spring Dependency Injection is a direct implementation of DIP:
- @Autowired
- Constructor injection
- @Qualifier
Summary
- SOLID is not academic — it directly impacts:
- Production bugs
- Change safety
- Team velocity
- Overusing SOLID is bad, ignoring SOLID is worse
- Aim for balance and clarity