SOLID Principles in LLD with Java Examples
SOLID is 5 rules for keeping code easy to change
S: Single Responsibility Principle (SRP)
There should be a reason to change a class. (one class should have 1 main responsibility). If there is a requirement change only one class responsible for that particular task should need change/modification.
A class can have multiple methods as long as they support the same responsibility.
How to identify Single Responsibility Principle (SRP) violation?
1) Check if a class performs multiple responsibilities?
2) Would different kinds of changes require to modify the same class?
3) Can we describe the class’s responsibility in one sentence without using “and” to join unrelated tasks?
Bad Example of Single Responsibility Principle: a single class doing all the tasks
The class ParkingLot has four responsibilities: Parking a vehicle, calculate parking fees, printing receipts, processing payments.
Suppose there is a change in any of these tasks for example:
Parking fee changes, we need to modify ParkingLot. Similarly, if receipt format changes we again modify ParkingLot.
Suppose we change parking fee for 2 wheelers, 4 wheelers and heavy vehicles different fees we might have to keep adding conditions inside ParkingLot. As the conditions keep on adding the class will become difficult to maintain. A better design could introduce separate fee calculation class.
Here ParkingLot class changes for multiple unrelated reasons which is a problem.
Good example of Single Responsibility Principle: Separate responsibilities
We can make different classes for different tasks.
- ParkingLot class to park vehicles, it can be changed if parking operations change in future.
- FeeCalculator class calculates parking fee. It can change if fee rules change.
- ReceiptPrinter class prints the receipts. It can be changed if receipt format changes.
- PaymentService class processes the payments. It can be changed for payment processing changes.
This solves our problem; we don’t have to change a class multiple times for unrelated reason anymore.
Summary: Single Responsibility Principle states a class should’ve only one reason to change. It should handle only one task. To describe this, we can use above ParkingLot example. The separation makes the code easier to maintain, test and modify when requirement changes.
O: Open/Closed Principle
Class should be open for extension but closed for modification. We should be able to add new functionality without repeatedly modifying existing, working code.
Bad Example: use if-else
Parking fees depending upon vehicle type
Requirements change:
- Car: 30 rupees/hr
- Bike: 10 rupees/hr
- Truck: 60 rupees/hr
Modify FeeCalculator to add another condition.
If weekend pricing is introduced, modify same class again. Every new pricing required modifying existing code, which breaks the already working code.
Good Example: Use an Interface
Define a common interface in place of keeping all fee rules in single class. Each class implements same interface but provide its own fee calculation.
New requirement can be introduced as another implementation. There is no need to change CarFeeStrategy, BikeFeeStrategy or TruckFeeStrategy.
The interface defines what the classes must do. Each implementation defines how calculation works.
How is this related to the Strategy Design Pattern?
What is Strategy Design Pattern?
Behavioural design pattern that lets you define multiple algorithms, put each one in a separate class, and switch between them without changing the class that uses them.
Instead of writing multiple if-else conditions for different behaviours, create separate classes for each behaviour and choose which one to use.
Example: without Strategy Pattern
Parking lot calculates fees based on vehicle type:
- Car: 30 rupees/hr
- Bike: 10 rupees/hr
- Truck: 60 rupees/hr
Introduction of new vehicle type will need modification of FeeCalculator and add another condition.
As no. of vehicle types and pricing rules increases, the class becomes harder to maintain
FeeStrategy defines common behaviour. Individual strategy classes implement that behaviour differently. ParkingLot uses the interface instead of depending on specific calculation logic.
Strategy Patterns allows to define different algorithms behind a common interface and switch between them.
Example: a ParkingLot receiving fee strategy instead of knowing the details of every pricing rule
To use different strategy, just provide another implementation without modifying ParkingLot.
Summary: Open/Closed Principle states that software entities should be open for extension but closed for modification. Addition of new functionality with minimal changes to the existing, tested code.
L: Liskov Substitution Principle (LSP)
Object of child class should be usable wherever object of the parent class are expected, without breaking the program’s behaviour.
Key idea: Don’t force child classes to support the behaviour that they cannot correctly perform.
Parking Lot example:
While building a parking lot system, it has parking spots, each spot can accept a vehicle when it is available. The spot can be Available, occupied, out of service.
Parking manager receives a ParkingSpot object and tries to park a vehicle in it. Expected behaviour is that the spot should handle parking request according to its rules.
Get Vaishnavi Parodkar| Software Engineer’s stories in your inbox
Join Medium for free to get updates from this writer.
Bad Design (Violation of LSP):
Any code receiving ParkingSpot can expect calling park() will perform parking operation.
manager.assignVehicle(normalSpot, car); //parks vehicle successfully
manager.assignVehicle(brokenSpot, car); //program fails, (UnsupportedOperationException) child class doesn’t honour the behaviour expected from its parent.
Here the substitution (brokenSpot) breaks the program’s expected behaviour, causing LSP violation.
Parking spot being unavailable is not LSP violation. The violation comes from child class breaking the parent’s promised contract rather than representing unavailability through agreed behaviour.
Good Design (Use state instead of a broken subclass)
Instead of creating a BrokenSpot subclass, represent the condition of a parking spot using an enum.
Because an out-of-service spot is still a parking spot. Its condition has changed, not its fundamental type.
park() method has a consistent contract: it returns true if parking succeeds and false if spot cannot accept a vehicle.
The program wont crash; it will handle the unavailable spot through the agreed return value.
Here the same class follows one consistent contract and represents different conditions through state.
Summary: LSP states that objects of a child class should be substitutable for objects of their parents class without changing the correctness of the program.
I: Interface Segregation Principle (ISP)
A class shouldn’t be forced to implement methods that it does not need. ISP doesn’t mean that every interface must have exactly one method. It means interfaces should be designed around the needs of their clients.
Bad example: One Large Interface with many methods
EmailNotifier only needs to send emails, but java forces it to implement all three methods because the interface requires them. We have unnecessary methods that either do nothing or throw exception. Violation of Interface Segregation Principle.
Good Example: Split the Interface
Each class implements only the functionality it actually supports.
Alternate Design — One common Interface
Interface with a single send() method. All notification channels perform the same conceptual operation.
Every channel supports the same operation: send(). Implementation differs, but the contract remains the same.
Use separate interfaces when the capabilities are different and clients may need only one particular capability.
Use one common interface when all implementations support the same operation and we want to interchange them easily.
Summary: Client shouldn’t be forced to depend on methods they do not use. Instead of creating large interfaces containing unrelated methods, we should divide them into smaller, specific interfaces.
D: Dependency Inversion Principle (DIP)
High-level classes should depend on abstractions, not on concrete implementations.
Bad Example: Hard-coded dependency
ParkingLot class creates its own HourlyFeeStrategy. Although the field uses FeeStrategy interface, the class is still tightly coupled to HourlyFeeStrategy because it directly creates that implementation.
We cannot simply choose the new strategy from outside the ParkingLot class. We must modify its internal construction logic or introduce another mechanism to select the implementation. This makes class harde to configure, extend, test.
Good Example: Dependency Injection
Instead of creating strategy inside ParkingLot, let another part of the application provide it.
ParkingLot class receives a FeeStrategy object from outside. It doesn’t need to know whether the supplied object is an HourlyFeeStrategy, WeekendFeeStrategy or another implementation.
No need to modify ParkingLot class to support weekend pricing. We simply supply new implementation. This is Dependency Injection. (Providing and object with its dependencies from outside rather than making it create them itself.)
Summary: A class shouldn’t be tightly connected to a specific implementation. Instead, it should depend on an interface and receive the implementation it needs. Both should depend on abstractions.
Summary of 5 SOLID principles:







