If you are preparing for Low-Level Design (LLD), Object-Oriented Design, or backend engineering interviews, SOLID principles are among the most important concepts to understand.
SOLID is a set of five object-oriented design principles that help developers create software that is:
- Easy to understand
- Easy to maintain
- Easy to extend
- Easy to test
- Less tightly coupled
- More reusable
The term SOLID represents five principles:
- S — Single Responsibility Principle
- O — Open/Closed Principle
- L — Liskov Substitution Principle
- I — Interface Segregation Principle
- D — Dependency Inversion Principle
These principles are frequently discussed in Java, Spring Boot, backend engineering, and system design interviews.
Why Do We Need SOLID Principles?
Imagine you are building an e-commerce application.
Initially, your code may contain only a few classes:
- Product
- Order
- Payment
- User
As the application grows, you may add:
- Discounts
- Multiple payment methods
- Notifications
- Refunds
- Inventory management
- Logging
- Analytics
Without proper design principles, classes quickly become tightly coupled.
A small change in payment logic may affect order processing.
Changing the notification provider might require modifying several classes.
Adding a new discount type may require changing existing working code.
SOLID principles help prevent these problems.
1. Single Responsibility Principle — SRP
The Single Responsibility Principle states:
A class should have only one reason to change.
In simpler words:
One class should be responsible for one specific job.
Consider an Invoice class.
A bad design might look like this:
public class Invoice {
public void calculateTotal() {
System.out.println("Calculating invoice total");
}
public void saveToDatabase() {
System.out.println("Saving invoice to database");
}
public void sendEmail() {
System.out.println("Sending invoice email");
}
}
This class is handling three responsibilities:
- Invoice calculations
- Database operations
- Email communication
If database logic changes, this class changes.
If email logic changes, this class changes.
If invoice calculations change, this class changes.
Therefore, the class has multiple reasons to change.
Better Design Using SRP
Separate each responsibility.
public class Invoice {
public void calculateTotal() {
System.out.println("Calculating invoice total");
}
}
Database responsibility:
public class InvoiceRepository {
public void save(Invoice invoice) {
System.out.println("Saving invoice to database");
}
}
Email responsibility:
public class InvoiceEmailService {
public void sendInvoice(Invoice invoice) {
System.out.println("Sending invoice email");
}
}
Now every class has a clearly defined responsibility.
Benefits of SRP
- Smaller classes
- Easier testing
- Easier debugging
- Fewer side effects
- Better maintainability
Real-World Example
In a Spring Boot application, you commonly separate:
Controller
|
v
Service
|
v
Repository
|
v
Database
The controller handles HTTP requests.
The service handles business logic.
The repository handles database operations.
This separation closely follows the Single Responsibility Principle.
2. Open/Closed Principle — OCP
The Open/Closed Principle states:
Software entities should be open for extension but closed for modification.
This means:
You should be able to add new functionality without constantly modifying existing working code.
Consider a payment system.
public class PaymentService {
public void pay(String paymentType) {
if (paymentType.equals("CARD")) {
System.out.println("Processing card payment");
}
if (paymentType.equals("UPI")) {
System.out.println("Processing UPI payment");
}
}
}
Now suppose you want to add:
- Net Banking
- Wallet
- PayPal
- Crypto
You will continuously modify the PaymentService.
if (paymentType.equals("WALLET")) {
...
}
if (paymentType.equals("NET_BANKING")) {
...
}
This violates OCP.
Better Design
Create an abstraction.
public interface PaymentMethod {
void pay(double amount);
}
Card payment:
public class CardPayment implements PaymentMethod {
@Override
public void pay(double amount) {
System.out.println(
"Paid ₹" + amount + " using card"
);
}
}
UPI payment:
public class UpiPayment implements PaymentMethod {
@Override
public void pay(double amount) {
System.out.println(
"Paid ₹" + amount + " using UPI"
);
}
}
Payment service:
public class PaymentService {
public void processPayment(
PaymentMethod paymentMethod,
double amount) {
paymentMethod.pay(amount);
}
}
Usage:
public class Main {
public static void main(String[] args) {
PaymentService paymentService =
new PaymentService();
PaymentMethod paymentMethod =
new UpiPayment();
paymentService.processPayment(
paymentMethod,
999
);
}
}
Output:
Paid ₹999.0 using UPI
Now adding another payment method does not require changing PaymentService.
For example:
public class WalletPayment implements PaymentMethod {
@Override
public void pay(double amount) {
System.out.println(
"Paid ₹" + amount + " using Wallet"
);
}
}
The system is now open for extension but closed for modification.
3. Liskov Substitution Principle — LSP
The Liskov Substitution Principle states:
Objects of a parent type should be replaceable with objects of their child types without breaking the program.
In simpler words:
If class B extends class A, then objects of B should behave in a way that users of A expect.
Example of LSP Violation
Consider:
public class Bird {
public void fly() {
System.out.println("Bird is flying");
}
}
Now create:
public class Sparrow extends Bird {
}
This works because sparrows can fly.
But consider:
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException(
"Penguins cannot fly"
);
}
}
Now:
public void makeBirdFly(Bird bird) {
bird.fly();
}
This works with:
makeBirdFly(new Sparrow());
But fails with:
makeBirdFly(new Penguin());
Therefore, Penguin cannot properly substitute Bird.
This violates LSP.
Better Design
Create a general bird abstraction.
public abstract class Bird {
public abstract void eat();
}
Create a separate interface for flying birds.
public interface Flyable {
void fly();
}
Sparrow:
public class Sparrow
extends Bird
implements Flyable {
@Override
public void eat() {
System.out.println("Sparrow is eating");
}
@Override
public void fly() {
System.out.println("Sparrow is flying");
}
}
Penguin:
public class Penguin extends Bird {
@Override
public void eat() {
System.out.println("Penguin is eating");
}
public void swim() {
System.out.println("Penguin is swimming");
}
}
Now the hierarchy correctly represents behavior.
Another Popular LSP Example: Rectangle and Square
Suppose:
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int area() {
return width * height;
}
}
A developer may try:
class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width;
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
But client code expecting independent width and height can behave unexpectedly.
This is a classic demonstration that inheritance is not always correct just because two objects appear related conceptually.
4. Interface Segregation Principle — ISP
The Interface Segregation Principle states:
Clients should not be forced to depend on methods they do not use.
In simpler words:
Avoid creating very large interfaces.
Instead, create smaller and more specific interfaces.
Bad Example
Consider:
public interface Worker {
void work();
void eat();
void sleep();
}
Now imagine a robot.
public class Robot implements Worker {
@Override
public void work() {
System.out.println("Robot working");
}
@Override
public void eat() {
throw new UnsupportedOperationException();
}
@Override
public void sleep() {
throw new UnsupportedOperationException();
}
}
The robot is forced to implement methods it does not need.
That violates ISP.
Better Design
Split the interface.
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
Human:
public class Human
implements Workable, Eatable, Sleepable {
@Override
public void work() {
System.out.println("Human working");
}
@Override
public void eat() {
System.out.println("Human eating");
}
@Override
public void sleep() {
System.out.println("Human sleeping");
}
}
Robot:
public class Robot implements Workable {
@Override
public void work() {
System.out.println("Robot working");
}
}
Now each class implements only the behavior it requires.
5. Dependency Inversion Principle — DIP
The Dependency Inversion Principle states:
High-level modules should not depend on low-level modules. Both should depend on abstractions.
It also states:
Abstractions should not depend on details. Details should depend on abstractions.
This principle is extremely important in backend development and is the foundation of Dependency Injection.
Bad Example
Suppose you have an email notification service.
public class EmailService {
public void send(String message) {
System.out.println(
"Email sent: " + message
);
}
}
Notification manager:
public class NotificationManager {
private EmailService emailService =
new EmailService();
public void notifyUser(String message) {
emailService.send(message);
}
}
NotificationManager directly depends on EmailService.
Now suppose you want to use:
- SMS
- Push notification
You must modify NotificationManager.
This creates tight coupling.
Better Design Using DIP
Create an abstraction.
public interface NotificationService {
void send(String message);
}
Email implementation:
public class EmailService
implements NotificationService {
@Override
public void send(String message) {
System.out.println(
"Email sent: " + message
);
}
}
SMS implementation:
public class SmsService
implements NotificationService {
@Override
public void send(String message) {
System.out.println(
"SMS sent: " + message
);
}
}
Notification manager:
public class NotificationManager {
private final NotificationService notificationService;
public NotificationManager(
NotificationService notificationService) {
this.notificationService =
notificationService;
}
public void notifyUser(String message) {
notificationService.send(message);
}
}
Usage:
public class Main {
public static void main(String[] args) {
NotificationService service =
new EmailService();
NotificationManager manager =
new NotificationManager(service);
manager.notifyUser(
"Your order has been placed"
);
}
}
Now NotificationManager depends on an abstraction instead of a concrete implementation.
Dependency Inversion vs Dependency Injection
These terms are related but not identical.
Dependency Inversion Principle is a software design principle.
Dependency Injection is one technique used to implement that principle.
For example:
public NotificationManager(
NotificationService service) {
this.notificationService = service;
}
This is constructor injection.
The dependency is provided from outside instead of being created internally.
SOLID Principles in Spring Boot
Spring Boot applications use SOLID concepts extensively.
Consider:
public interface PaymentService {
void pay(double amount);
}
Implementation:
@Service
public class UpiPaymentService
implements PaymentService {
@Override
public void pay(double amount) {
System.out.println(
"Processing UPI payment"
);
}
}
Controller:
@RestController
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(
PaymentService paymentService) {
this.paymentService = paymentService;
}
}
The controller depends on an abstraction.
Spring injects the implementation.
This follows the Dependency Inversion Principle and uses Dependency Injection.
How SOLID Principles Work Together
SOLID principles are not isolated rules.
They often work together.
Consider a notification system.
NotificationService
|
-----------------------------
| | |
Email SMS WhatsApp
Service Service Service
SRP ensures every service handles one responsibility.
OCP allows you to add another notification type without changing existing logic.
LSP ensures every notification implementation can replace the abstraction correctly.
ISP helps keep interfaces small and focused.
DIP ensures high-level business logic depends on NotificationService rather than a specific provider.
SOLID Example: Payment System
Consider this architecture:
PaymentProcessor
|
v
PaymentMethod
/ | \
/ | \
UPI Card Wallet
Interface:
public interface PaymentMethod {
void pay(double amount);
}
Implementations:
public class UpiPayment
implements PaymentMethod {
@Override
public void pay(double amount) {
System.out.println(
"UPI payment: ₹" + amount
);
}
}
public class CardPayment
implements PaymentMethod {
@Override
public void pay(double amount) {
System.out.println(
"Card payment: ₹" + amount
);
}
}
Processor:
public class PaymentProcessor {
private final PaymentMethod paymentMethod;
public PaymentProcessor(
PaymentMethod paymentMethod) {
this.paymentMethod = paymentMethod;
}
public void process(double amount) {
paymentMethod.pay(amount);
}
}
This design is easy to extend.
To add Net Banking:
public class NetBankingPayment
implements PaymentMethod {
@Override
public void pay(double amount) {
System.out.println(
"Net banking payment: ₹" + amount
);
}
}
Existing classes do not need modification.
Common SOLID Interview Questions
What does SOLID stand for?
SOLID represents:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
What is the difference between SRP and ISP?
SRP applies mainly to classes and responsibilities.
A class should have one primary reason to change.
ISP applies mainly to interfaces.
An interface should not force clients to implement methods they do not need.
What is the difference between OCP and DIP?
OCP focuses on making software extensible without modifying existing code.
DIP focuses on reducing coupling by depending on abstractions rather than concrete implementations.
Both principles often work together.
Is inheritance required for SOLID?
No.
In fact, excessive inheritance can create tightly coupled designs.
Composition is frequently preferred.
Example:
Instead of:
Car extends Engine
prefer:
Car HAS-A Engine
public class Car {
private Engine engine;
}
Can SOLID principles be overused?
Yes.
Not every small class needs multiple abstractions, interfaces, factories, and layers.
For a very small application, excessive abstractions may make the code harder to understand.
SOLID should be applied when it improves:
- Flexibility
- Maintainability
- Testability
- Extensibility
The objective is better design, not simply creating more interfaces.
Easy Way to Remember SOLID
Use this quick summary:
PrincipleRemember It AsSRPOne class, one responsibilityOCPAdd new behavior without changing old codeLSPChild classes must safely replace parent classesISPKeep interfaces small and focusedDIPDepend on abstractions, not implementations
SOLID Principles and Low-Level Design Interviews
In an LLD interview, interviewers usually do not expect you to simply define SOLID.
They want to see whether your design naturally follows these principles.
Suppose you are asked to design a Parking Lot.
Instead of writing:
if(vehicleType.equals("CAR")) {
}
else if(vehicleType.equals("BIKE")) {
}
else if(vehicleType.equals("TRUCK")) {
}
you could use abstractions such as:
public abstract class Vehicle {
private String registrationNumber;
}
and concrete implementations:
Vehicle | ---------------- | | | Car Bike Truck
Similarly, pricing strategies can be separated:
PricingStrategy
|
----------------------
| |
HourlyPricing FlatPricing
This makes it easier to support new pricing models without modifying existing logic.
This is where SOLID becomes useful in real LLD interviews.
SOLID vs Design Patterns
SOLID principles and design patterns are related but different.
SOLID principles provide guidelines for designing maintainable software.
Design patterns provide reusable solutions to commonly occurring design problems.
For example:
The Strategy Pattern can help implement OCP.
The Factory Pattern can reduce coupling.
Dependency Injection can help implement DIP.
Observer Pattern can improve extensibility in event-driven systems.
A strong LLD solution usually combines:
OOP + SOLID Principles + Design Patterns + Clean Code + Proper Requirement Analysis
Final Takeaway
SOLID principles are not simply theoretical concepts for interviews.
They help developers create software that can grow without becoming difficult to maintain.
Remember:
S — Single Responsibility
One class should handle one primary responsibility.
O — Open/Closed
Extend behavior without modifying stable existing code.
L — Liskov Substitution
Subtypes should correctly replace their parent abstractions.
I — Interface Segregation
Do not force clients to depend on unnecessary methods.
D — Dependency Inversion
High-level business logic should depend on abstractions rather than concrete implementations.
When solving LLD problems such as:
- Parking Lot
- BookMyShow
- Splitwise
- Payment Gateway
- Notification System
- Rate Limiter
- Inventory Management
- Cab Booking System
try to identify where SOLID principles can make your design easier to extend and maintain.
That is far more important than simply memorizing their definitions.