| Users / Codebase Size | Code Complexity | Maintainability | Bug Rate | Refactoring Effort |
|---|---|---|---|---|
| 100 users / Small code | Low | High | Low | Minimal |
| 10K users / Medium code | Moderate | Moderate to High | Moderate | Moderate |
| 1M users / Large code | High | Moderate | High | High |
| 100M users / Very large code | Very High | Low without SOLID | Very High | Very High |
Applying SOLID to real code in LLD - Scalability & System Analysis
As the codebase grows, the first bottleneck is code maintainability. Without SOLID principles, the code becomes tightly coupled and hard to change. This leads to more bugs and slower feature delivery.
- Single Responsibility Principle: Break code into small classes/functions each with one job. Easier to test and change.
- Open/Closed Principle: Design code to add new features by adding code, not changing existing code. Use interfaces and inheritance.
- Liskov Substitution Principle: Subclasses should work wherever base classes are used. Prevents unexpected bugs.
- Interface Segregation Principle: Use many small, specific interfaces instead of one big one. Clients depend only on what they use.
- Dependency Inversion Principle: Depend on abstractions, not concrete classes. Makes swapping implementations easier.
Applying these principles helps the code scale in size and complexity without breaking.
For a growing codebase:
- Bug fixes and feature additions take longer without SOLID, increasing developer hours by 2x to 5x at large scale.
- Refactoring legacy code without SOLID can cost weeks or months of effort.
- Applying SOLID early reduces long-term maintenance cost by improving modularity and testability.
- Code review and onboarding new developers is faster with clear, SOLID-based design.
When discussing scalability of code design, start by explaining the challenges of growing codebases. Then introduce SOLID principles as a way to keep code modular and maintainable. Use simple examples to show how each principle helps avoid common problems. Finally, mention trade-offs like initial design effort versus long-term benefits.
Your codebase is growing and becoming hard to maintain. What SOLID principle do you apply first and why?
Answer: Start with the Single Responsibility Principle to break large classes into smaller ones with one job each. This reduces complexity and makes future changes easier.
Practice
Solution
Step 1: Understand the principle names
Single Responsibility Principle means one class should do one job only.Step 2: Match principle to description
Only Single Responsibility Principle talks about one reason to change.Final Answer:
Single Responsibility Principle -> Option DQuick Check:
One class, one reason to change = Single Responsibility Principle [OK]
- Confusing Single Responsibility with Open/Closed
- Thinking Dependency Inversion means single responsibility
- Mixing Liskov Substitution with class responsibilities
Solution
Step 1: Identify Dependency Inversion Principle
It says depend on abstractions (interfaces), not concrete classes.Step 2: Check which code uses interfaces
interface Engine { void start(); } class Car { Engine engine; Car(Engine e) { engine = e; } } uses interface Engine and injects it into Car, following DIP.Final Answer:
Interface with injection to depend on abstraction -> Option BQuick Check:
Depend on interface, not concrete class = DIP [OK]
- Creating concrete objects inside classes (violates DIP)
- Extending concrete classes instead of using interfaces
- Not using constructor or setter injection
interface Shape { double area(); }
class Rectangle implements Shape {
double width, height;
Rectangle(double w, double h) { width = w; height = h; }
public double area() { return width * height; }
}
class Circle implements Shape {
double radius;
Circle(double r) { radius = r; }
public double area() { return 3.14 * radius * radius; }
}
class AreaCalculator {
double totalArea(Shape[] shapes) {
double total = 0;
for (Shape s : shapes) total += s.area();
return total;
}
}
public class Main {
public static void main(String[] args) {
Shape[] shapes = { new Rectangle(2,3), new Circle(1) };
System.out.println(new AreaCalculator().totalArea(shapes));
}
}Solution
Step 1: Calculate Rectangle area
Rectangle area = 2 * 3 = 6Step 2: Calculate Circle area
Circle area = 3.14 * 1 * 1 = 3.14Step 3: Sum areas
Total area = 6 + 3.14 = 9.14Final Answer:
9.14 -> Option AQuick Check:
6 + 3.14 = 9.14 [OK]
- Using wrong formula for circle area
- Adding radius instead of area
- Forgetting to sum both shapes
class Report {
void printReport(String type) {
if (type.equals("PDF")) { System.out.println("Printing PDF"); }
else if (type.equals("HTML")) { System.out.println("Printing HTML"); }
}
}Solution
Step 1: Understand Open/Closed Principle
Classes should be open for extension but closed for modification.Step 2: Analyze printReport method
Adding new types means editing if-else, so modifying existing code.Final Answer:
Adding new report types requires changing printReport method -> Option AQuick Check:
Modifying code to add features breaks Open/Closed Principle [OK]
- Thinking if-else is always fine
- Confusing Open/Closed with Single Responsibility
- Ignoring need for polymorphism
Solution
Step 1: Understand Interface Segregation Principle
Clients should not depend on methods they do not use; interfaces should be small and specific.Step 2: Apply to payment system
Separate interfaces for each payment type avoid forcing classes to implement unused methods.Final Answer:
Create separate interfaces like CreditCardPaymentInterface and PayPalPaymentInterface with only relevant methods -> Option CQuick Check:
Small, specific interfaces = Interface Segregation Principle [OK]
- Using one big interface for all payment types
- Inheriting unrelated payment classes
- Using abstract class forcing all methods
Ready to prove it?
Pass the test and this pattern moves unknown
