Introduction to LLD and SOLID
In System Design (HLD), we worry about shards, replication, and availability. In Low-Level Design (LLD), we worry about change.
A poor LLD means a simple requirement change requires touching 15 files. A good LLD means adding a new feature is as simple as adding a new class. The bridge between these two worlds is the SOLID principles.
Most engineers can recite what SOLID stands for. Few can apply them to a production codebase without over-engineering. This guide focuses on the practical application of SOLID in modern Java backend services.
1. Single Responsibility Principle (SRP)
"A class should have one, and only one, reason to change."
The Violation: A UserService that handles user validation, database persistence, email notifications, and JWT token generation.
The Fix: Break the service down.
UserValidator: Handles business rules.UserRepository: Handles database interaction.NotificationService: Handles communication.TokenProvider: Handles security tokens.
Senior Tip: If you find yourself using the word "and" when describing what your class does, you are likely violating SRP.
2. Open/Closed Principle (OCP)
"Software entities should be open for extension, but closed for modification."
The Violation: Using a large switch statement to handle different payment methods. Every time you add "Crypto", you have to modify the existing PaymentProcessor class.
The Fix: The Strategy Pattern.
public interface PaymentStrategy {
void process(double amount);
}
public class PaymentProcessor {
private final Map<String, PaymentStrategy> strategies;
public void executePayment(String type, double amount) {
strategies.get(type).process(amount);
}
}
Now, adding a new payment method requires adding a new class, zero changes to the PaymentProcessor.
3. Liskov Substitution Principle (LSP)
"Objects of a superclass should be replaceable with objects of its subclasses without breaking the application."
The Violation: A Square class extending a Rectangle class where setting the width also changes the height. This breaks the expectation of the client code using the Rectangle interface.
The Fix: Favor composition over inheritance or ensure that the subclass strictly adheres to the contract of the parent. If a subclass throws an UnsupportedOperationException for a method defined in the parent, you are likely violating LSP.
4. Interface Segregation Principle (ISP)
"Clients should not be forced to depend upon interfaces that they do not use."
The Violation: A Worker interface with work() and eat(). If you have a RobotWorker, it is forced to implement eat(), which it doesn't need.
The Fix: Split the interfaces.
Workable { void work(); }Feedable { void eat(); }
Senior Tip: In Java, default methods in interfaces can be a trap. Ensure they don't force unrelated behavior on implementers.
5. Dependency Inversion Principle (DIP)
"Depend upon abstractions, not concretions."
The Violation: OrderService directly instantiating MySQLUserRepository. If you want to switch to MongoDB, you have to modify OrderService.
The Fix: Dependency Injection.
public class OrderService {
private final UserRepository repository; // Interface, not implementation
public OrderService(UserRepository repository) {
this.repository = repository;
}
}
By injecting the dependency (usually via Spring's @Autowired or constructor injection), OrderService remains agnostic of the underlying database technology.
When to Ignore SOLID?
Over-applying SOLID leads to "Class Explosion"—a codebase with thousands of tiny, 5-line classes that are impossible to navigate.
The Rule of Three: Don't apply OCP or ISP the first time you write a feature. Apply it the third time you have to modify that area of code. Premature abstraction is the root of all evil in LLD.
Final Takeaways
- SRP keeps your classes small and testable.
- OCP makes your system pluggable.
- LSP ensures your abstractions are honest.
- ISP prevents "Fat Interfaces."
- DIP decouples your business logic from your infrastructure.