SOLID
SOLID is an acronym for five design principles that makes software designs more understandable, flexible, and maintainable.
Single Responsibility Principle (SRP)
A class should have one and only one reason to change.
Bad Example:
class Order {
void calculateTotal(){
// calculate order
}
void savetoDB(){
// save order to database
}
void sendEmail(){
// send confirmation email
}
}This class has several responsibilities:
- Business logic -> calculate total
- Database -> save order
- Communication -> send email
- File/document generation -> invoice
Now imagine the email system changes from SMTP to an API. You have to modify Order.
If the database changes from MySQL to MongoDB, you modify Order again.
If invoice generation changes, you modify Order again.
That’s a violation of SRP.
Applying SRP:
Separate the responsibilities:
class Order {
void calculate(){
// business logic
}
}
class OrderRepository {
void save(Order order) {
// database logic
}
}
class EmailService {
void sendConfirmation(Order order) {
// email logic
}
}Now each class has a much clearer responsibility:
Order
↓
Business logic
OrderRepository
↓
Database
EmailService
↓
Email
InvoiceService
↓
InvoiceWhy is this useful in real projects? Imagine you are working on a Spring Boot project. Each layer has a focused responsibility:
- Controller: HTTP/request handling
- Service: Business logic
- Repository: Data access
One class = one cohesive responsibility = one major reason to change.
Open / Closed Principle (OCP)
Software entities should be open for extension, but closed for modification.
You should be able to add new behavior without changing existing, tested code.
Bad Example
class PaymentService {
void pay(String type) {
if (type.equals("UPI")) {
System.out.println("Pay using UPI");
}
else if (type.equals("CARD")) {
System.out.println("Pay using Card");
}
else if (type.equals("PAYPAL")) {
System.out.println("Pay using PayPal");
}
}
}Good Example: Applying OCP
Create an abstraction:
interface Payment {
void pay();
}Then create implementations:
class UPI implements Payment {
public void pay() {
System.out.println("Pay using UPI");
}
}
class Card implements Payment {
public void pay() {
System.out.println("Pay using Card");
}
}
class PayPal implements Payment {
public void pay() {
System.out.println("Pay using PayPal");
}
}Now the service doesn’t care which payment method is being used:
class PaymentService {
void processPayment(Payment payment) {
payment.pay();
}
}Now suppose you want Apple Pay. You don’t modify PaymentService. You simply extend the system:
class ApplePay implements Payment {
public void pay() {
System.out.println("Pay using Apple Pay");
}
}That’s OCP.
Payment
│
┌────────────┼────────────┐
↓ ↓ ↓
UPI Card PayPal
│
ApplePayLiskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without altering program correctness.
In simpler words:
If
Bis a subtype ofA, you should be able to useBwhereverAis expected, and the program should still work correctly.
Practical example: Bad design
Consider a bird hierarchy:
class Bird {
void fly() {
System.out.println("Flying");
}
}Now:
class Sparrow extends Bird {
// Sparrow can fly
}But:
class Penguin extends Bird {
@Override
void fly() {
throw new UnsupportedOperationException("Penguins can't fly");
}
}Now suppose we have:
void makeBirdFly(Bird bird) {
bird.fly();
}This works:
makeBirdFly(new Sparrow());But this breaks:
makeBirdFly(new Penguin());The problem is that Penguin claims to be a Bird, but it cannot fulfill the behavior expected from Bird. Therefore, the subtype violates LSP.
Better design
Don’t put fly() in the common Bird class.
class Bird {
void eat() {
System.out.println("Eating");
}
}Then create a separate abstraction for flying:
interface Flyable {
void fly();
}Now:
class Sparrow extends Bird implements Flyable {
public void fly() {
System.out.println("Sparrow flying");
}
}And:
class Penguin extends Bird {
// Doesn't implement Flyable
}Now the types correctly represent their capabilities.
Bird
/ \
Sparrow Penguin
|
FlyableAnother very practical example
Imagine:
class Rectangle {
protected int width;
protected int height;
void setWidth(int width) {
this.width = width;
}
void setHeight(int height) {
this.height = height;
}
int getArea() {
return width * height;
}
}You might think:
class Square extends Rectangle {
@Override
void setWidth(int width) {
this.width = width;
this.height = width;
}
@Override
void setHeight(int height) {
this.width = height;
this.height = height;
}
}Mathematically, a square is a rectangle. But from a software-behavior perspective, substituting Square for Rectangle can break assumptions.
For example:
void resize(Rectangle r) {
r.setWidth(10);
r.setHeight(20);
System.out.println(r.getArea());
}For a normal rectangle: 10 × 20 = 200
For the Square implementation: 20 × 20 = 400
So the subtype changes the expected behavior of the base type. That’s an LSP violation.
The key idea
LSP isn’t really about inheritance syntax. It’s about behavior. If you write:
Animal animal = new Dog();then Dog should behave in a way that makes sense wherever the program expects an Animal.
Bad:
Animal animal = new Dog();
animal.makeSound(); // expected to workbut Dog throws an exception or behaves completely differently from what Animal promises.
Interview definition:
LSP means that a subclass should be usable anywhere its superclass is expected without causing unexpected behavior, violating the superclass’s contract, or breaking program correctness.
Interface Segregation Principle (ISP)
Clients should not be forced to depend on methods they do not use.
The easiest way to understand ISP is:
Don’t create one huge interface. Split it into smaller interfaces based on what different classes actually need.
Practical example
Suppose you have:
interface Machine {
void print();
void scan();
void fax();
}Now you create a simple printer:
class SimplePrinter implements Machine {
public void print() {
System.out.println("Printing");
}
public void scan() {
// I don't scan!
}
public void fax() {
// I don't fax!
}
}The SimplePrinter is forced to implement scan() and fax(), even though it doesn’t need them. That’s an ISP violation.
Fix: split the interface
interface Printer {
void print();
}
interface Scanner {
void scan();
}
interface Fax {
void fax();
}Now:
class SimplePrinter implements Printer {
public void print() {
System.out.println("Printing");
}
}A multifunction machine can implement all three:
class MultiFunctionPrinter implements Printer, Scanner, Fax {
public void print() {}
public void scan() {}
public void fax() {}
}Now nobody is forced to implement something irrelevant.
Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
The simple idea is:
Your important business logic should not be tightly coupled to specific implementation details.
First, understand the problem
Suppose you build an application that sends notifications. You write:
class EmailService {
void sendEmail(String message) {
System.out.println("Sending email");
}
}And your business logic directly uses it:
class OrderService {
private EmailService emailService = new EmailService();
void placeOrder() {
// order logic
emailService.sendEmail("Order placed");
}
}The dependency is:
OrderService
↓
EmailServiceOrderService is high-level business logic.
EmailService is a low-level implementation detail.
What’s the problem?
Tomorrow you want to use SMS or WhatsApp. You have to modify OrderService. Your important business logic is tightly coupled to specific technologies.
Apply DIP
Create an abstraction:
interface NotificationService {
void send(String message);
}Now implementations depend on that abstraction:
class EmailService implements NotificationService {
public void send(String message) {
System.out.println("Sending Email");
}
}
class SMSService implements NotificationService {
public void send(String message) {
System.out.println("Sending SMS");
}
}Now OrderService also depends on the interface, not Email specifically:
class OrderService {
private NotificationService notificationService;
OrderService(NotificationService notificationService) {
this.notificationService = notificationService;
}
void placeOrder() {
// business logic
notificationService.send("Order placed");
}
}Now the dependency looks like:
HIGH LEVEL
OrderService
↓
NotificationService
(interface)
↑
LOW LEVEL
EmailService / SMSServiceBoth sides depend on the abstraction.
All SOLID together
You can now understand the five principles as problems they prevent:
S — SRP
"Why does this class have so many responsibilities?"
O — OCP
"Why must I modify existing code every time I add something?"
L — LSP
"Why can't this child properly behave like its parent?"
I — ISP
"Why am I forced to implement methods I don't need?"
D — DIP
"Why is my important business logic directly tied to this specific implementation?"Memory trick
| Principle | Think |
|---|---|
| SRP | One class → focused responsibility |
| OCP | Add behavior without modifying existing code |
| LSP | Child → must honor parent’s contract |
| ISP | Interface → don’t force unnecessary methods |
| DIP | Depend on abstractions, not concrete implementations |