Introduction
SOLID is an acronym for five design principles that help developers build software that is easy to maintain, extend, and test. These principles were popularized by Robert C. Martin (Uncle Bob) and are the foundation of professional object-oriented design.
S — Single Responsibility Principle (SRP)
A class should have only one reason to change.
Bad Code
public class UserService {
public void registerUser(User user) {
// Validate
if (user.getEmail() == null) throw new IllegalArgumentException("Email required");
// Save to DB
Connection conn = DriverManager.getConnection("...");
PreparedStatement ps = conn.prepareStatement("INSERT INTO users ...");
ps.executeUpdate();
// Send welcome email
EmailClient client = new EmailClient();
client.send(user.getEmail(), "Welcome!", "...");
// Log audit
AuditLogger.log("User registered: " + user.getEmail());
}
}
Problems: This class changes if validation rules change, if the database changes, if email templates change, or if audit requirements change. Four reasons to change.
Refactored Code
public class UserRegistrationService {
private final UserValidator validator;
private final UserRepository userRepository;
private final WelcomeEmailSender emailSender;
private final AuditLogger auditLogger;
public void registerUser(User user) {
validator.validate(user);
userRepository.save(user);
emailSender.sendWelcomeEmail(user);
auditLogger.logRegistration(user);
}
}
Each collaborator has exactly one reason to change.
O — Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
Bad Code
public class PaymentProcessor {
public void processPayment(String type, double amount) {
if ("UPI".equals(type)) {
// UPI logic
} else if ("CARD".equals(type)) {
// Card logic
} else if ("NET_BANKING".equals(type)) {
// Net banking logic
}
// Every new payment method = modify this class
}
}
Refactored Code
public interface PaymentStrategy {
void pay(double amount);
}
public class UpiPayment implements PaymentStrategy {
public void pay(double amount) { /* UPI logic */ }
}
public class CardPayment implements PaymentStrategy {
public void pay(double amount) { /* Card logic */ }
}
public class NetBankingPayment implements PaymentStrategy {
public void pay(double amount) { /* Net banking logic */ }
}
public class PaymentProcessor {
private final PaymentStrategy strategy;
public PaymentProcessor(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void processPayment(double amount) {
strategy.pay(amount);
}
}
Adding a new payment method (e.g., Crypto) means creating a new class — zero modification to existing code.
L — Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without altering program correctness.
Classic Example: Bird / Penguin
public class Bird {
public void fly() {
System.out.println("Flying...");
}
}
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly!");
}
}
This violates LSP. Any code that calls bird.fly() will break if it receives a Penguin.
Refactored Code
public abstract class Bird {
public abstract void move();
}
public class Sparrow extends Bird {
public void move() { System.out.println("Flying..."); }
}
public class Penguin extends Bird {
public void move() { System.out.println("Swimming..."); }
}
Real-World Example: Payment Gateway
// Bad — violates LSP
public class FreeTrialAccount extends PaymentAccount {
@Override
public void charge(double amount) {
throw new UnsupportedOperationException("Free trial can't be charged");
}
}
// Good — separate interfaces
public interface Chargeable {
void charge(double amount);
}
public interface AccountInfo {
String getAccountId();
String getPlan();
}
public class PaidAccount implements Chargeable, AccountInfo { ... }
public class FreeTrialAccount implements AccountInfo { ... }
I — Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
Bad Code
public interface Employee {
void work();
void attendMeeting();
void generateReport();
void codeReview();
void manageSprint();
void conductInterview();
void approveLeave();
}
A junior developer is forced to implement approveLeave() and manageSprint(), which they never do.
Refactored Code
public interface Worker {
void work();
}
public interface MeetingAttendee {
void attendMeeting();
}
public interface SprintManager {
void manageSprint();
void approveLeave();
}
public interface CodeReviewer {
void codeReview();
}
// A junior implements only what they need
public class JuniorDeveloper implements Worker, MeetingAttendee { ... }
// A tech lead implements more
public class TechLead implements Worker, MeetingAttendee, CodeReviewer, SprintManager { ... }
D — Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Evolution: From Tight Coupling to Spring Boot
Step 1 — Tight Coupling (Bad)
public class NotificationService {
private EmailSender emailSender = new EmailSender(); // Hardcoded dependency
public void notify(String message) {
emailSender.send(message);
}
}
Step 2 — Manual Dependency Injection
public class NotificationService {
private final MessageSender sender;
public NotificationService(MessageSender sender) {
this.sender = sender;
}
public void notify(String message) {
sender.send(message);
}
}
// Usage
NotificationService service = new NotificationService(new EmailSender());
NotificationService smsService = new NotificationService(new SmsSender());
Step 3 — Spring Boot DI
public interface MessageSender {
void send(String message);
}
@Component
public class EmailSender implements MessageSender {
public void send(String message) { /* send email */ }
}
@Service
public class NotificationService {
private final MessageSender sender;
@Autowired
public NotificationService(MessageSender sender) {
this.sender = sender;
}
}
Step 4 — Testability
@Test
void shouldSendNotification() {
MessageSender mockSender = mock(MessageSender.class);
NotificationService service = new NotificationService(mockSender);
service.notify("Hello");
verify(mockSender).send("Hello");
}
DIP enables testability. You can inject a mock instead of a real email sender.
Interview Questions
-
Which SOLID principle is most important? They are all interconnected, but SRP is the foundation. If each class has one responsibility, the others naturally follow.
-
How does OCP relate to design patterns? Strategy, Observer, and Decorator patterns are direct implementations of OCP — they allow extending behavior without modifying existing code.
-
Give a real-world LSP violation. Java's
StackextendingVector. Stack is LIFO, but inheriting from Vector exposesadd(index, element)which breaks the LIFO contract.
Common Mistakes
- Over-abstracting: Creating interfaces for classes that will only ever have one implementation. SOLID is about managing complexity, not creating it.
- Applying all principles at once: Start with SRP. The rest become natural as the codebase grows.
- Ignoring DIP in tests: If your unit tests need a running database or email server, you have a DIP violation.
Summary
| Principle | One-Liner |
|---|---|
| SRP | One class, one reason to change |
| OCP | Extend behavior without modifying code |
| LSP | Subtypes must be drop-in replacements |
| ISP | Small, focused interfaces |
| DIP | Depend on abstractions, not concretions |