Introduction
As software systems grow, code often becomes difficult to maintain, extend, and test. New features introduce unexpected bugs, small changes require modifications in multiple places, and development velocity gradually decreases.
To address these challenges, Robert C. Martin (Uncle Bob) introduced the SOLID Principles, a set of five object-oriented design principles that help developers create software that is:
- Easy to understand
- Easy to extend
- Easy to test
- Easy to maintain
- Resilient to change
Whether you’re building a simple Spring Boot application or a large-scale microservices platform, understanding SOLID principles is essential for every Java developer.
What is SOLID?
SOLID is an acronym representing five design principles:
| Principle | Meaning |
|---|---|
| S | Single Responsibility Principle |
| O | Open Closed Principle |
| L | Liskov Substitution Principle |
| I | Interface Segregation Principle |
| D | Dependency Inversion Principle |
Let’s explore each principle with Java examples.
S – Single Responsibility Principle (SRP)
Definition
A class should have only one reason to change.
A class should be responsible for only one piece of functionality.
Bad Example
public class EmployeeService {
public void saveEmployee(Employee employee) {
// Save employee
}
public void generateReport(Employee employee) {
// Generate PDF report
}
public void sendEmail(Employee employee) {
// Send email
}
}
Problems
The class has multiple responsibilities:
- Employee persistence
- Report generation
- Email notification
Changes in any of these areas impact the same class.
Good Example
public class EmployeeService {
public void saveEmployee(Employee employee) {
// Save employee
}
}
public class ReportService {
public void generateReport(Employee employee) {
// Generate report
}
}
public class EmailService {
public void sendEmail(Employee employee) {
// Send email
}
}
Each class now has a single responsibility.
Spring Boot Example
Common services:
@Service
public class CustomerService {}
@Service
public class NotificationService {}
@Service
public class AuditService {}
Each service handles one concern.
O – Open Closed Principle (OCP)
Definition
Software entities should be open for extension but closed for modification.
We should be able to add new behavior without changing existing code.
Bad Example
public class PaymentService {
public void pay(String type) {
if(type.equals("CARD")) {
System.out.println("Card Payment");
}
else if(type.equals("UPI")) {
System.out.println("UPI Payment");
}
}
}
Every new payment method requires modification.
Good Example Using Strategy Pattern
Step 1
public interface PaymentStrategy {
void pay(double amount);
}
Step 2
public class CardPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Card Payment");
}
}
public class UpiPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("UPI Payment");
}
}
Step 3
public class PaymentService {
private final PaymentStrategy strategy;
public PaymentService(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void pay(double amount) {
strategy.pay(amount);
}
}
Adding a new payment type requires creating a new implementation only.
Real World Examples
- Spring Security Authentication Providers
- Message Converters
- Notification Channels
- Payment Gateways
L – Liskov Substitution Principle (LSP)
Definition
Objects of a superclass should be replaceable with objects of its subclasses without breaking behavior.
Bad Example
class Bird {
public void fly() {
System.out.println("Flying");
}
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException();
}
}
Problem:
A Penguin is not substitutable for Bird.
Better Design
interface Bird {
}
interface FlyingBird extends Bird {
void fly();
}
class Sparrow implements FlyingBird {
@Override
public void fly() {
System.out.println("Flying");
}
}
class Penguin implements Bird {
}
Now the hierarchy reflects reality.
Why LSP Matters
Violating LSP causes:
- Unexpected runtime failures
- Broken polymorphism
- Fragile inheritance structures
I – Interface Segregation Principle (ISP)
Definition
Clients should not be forced to depend on methods they do not use.
Bad Example
public interface Worker {
void work();
void eat();
void sleep();
}
A Robot worker doesn’t eat or sleep.
public class RobotWorker implements Worker {
@Override
public void work() {}
@Override
public void eat() {
throw new UnsupportedOperationException();
}
@Override
public void sleep() {
throw new UnsupportedOperationException();
}
}
Good Example
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
public class HumanWorker
implements Workable,
Eatable,
Sleepable {
}
public class RobotWorker
implements Workable {
}
Spring Example
Instead of one giant interface:
CrudRepository
PagingAndSortingRepository
JpaRepository
Spring follows ISP extensively.
D – Dependency Inversion Principle (DIP)
Definition
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Bad Example
public class NotificationService {
private EmailService emailService =
new EmailService();
public void send() {
emailService.send();
}
}
Tightly coupled.
Good Example
Abstraction
public interface NotificationChannel {
void send();
}
Implementations
public class EmailService
implements NotificationChannel {
public void send() {
System.out.println("Email");
}
}
public class SmsService
implements NotificationChannel {
public void send() {
System.out.println("SMS");
}
}
Consumer
public class NotificationService {
private NotificationChannel channel;
public NotificationService(
NotificationChannel channel) {
this.channel = channel;
}
public void send() {
channel.send();
}
}
Spring Boot Example
@Service
public class NotificationService {
private final NotificationChannel channel;
@Autowired
public NotificationService(
NotificationChannel channel) {
this.channel = channel;
}
}
Spring automatically injects the implementation.
SOLID and Design Patterns
| Principle | Common Design Pattern |
|---|---|
| SRP | Service Layer |
| OCP | Strategy Pattern |
| LSP | Proper Inheritance |
| ISP | Small Interfaces |
| DIP | Dependency Injection |
SOLID in Microservices
Consider a Notification Microservice:
SRP
Separate services for:
- SMS
- Push
OCP
Add WhatsApp notifications without changing existing code.
LSP
All notification providers behave consistently.
ISP
Small interfaces:
EmailSender
SmsSender
PushSender
DIP
Business logic depends on:
NotificationChannel
instead of:
EmailProvider
SmsProvider
Common Interview Questions
Q1. What does SOLID stand for?
Single Responsibility, Open Closed, Liskov Substitution, Interface Segregation, Dependency Inversion.
Q2. Which SOLID principle is most commonly violated?
Single Responsibility Principle.
Q3. Which design pattern best demonstrates OCP?
Strategy Pattern.
Q4. Which SOLID principle does Spring Dependency Injection support?
Dependency Inversion Principle.
Q5. Does using interfaces automatically satisfy SOLID?
No.
Poor interface design can still violate ISP and LSP.
Benefits of SOLID Principles
Following SOLID results in:
- Better maintainability
- Easier testing
- Reduced coupling
- Increased extensibility
- Better team productivity
- Easier microservice evolution
Conclusion
SOLID principles are not just interview topics—they are practical guidelines for building maintainable software. Modern frameworks such as Spring Boot, Spring Security, Spring Data JPA, and Microservices architectures heavily rely on these principles.
A good rule of thumb is:
- Keep classes focused (SRP)
- Extend behavior without modifying existing code (OCP)
- Design inheritance carefully (LSP)
- Create small, focused interfaces (ISP)
- Depend on abstractions, not implementations (DIP)
Mastering SOLID principles is one of the fastest ways to transition from writing code that works to designing systems that scale and evolve gracefully.