Opening low-level design
Progress is saved as you study.
Opening low-level design
Progress is saved as you study.
Opening your learning path
Progress is saved as you study.
SOLID Principles is a core lesson in the OOP Foundations & Class Design family of the Thita low level design course.
OOP pillars, SOLID principles, entity modeling and class extraction.
It is part of the Interview Soon path: the LLD material that appears in a standard interview loop rather than only in a specialist one.
SOLID shows up in interviews when your design starts small and the interviewer adds requirements: “support wallet payments,” “send SMS instead of email,” “some devices cannot fax,” or “discount rules will keep changing.”
Candidates fail when they recite the acronym and still write classes that need edits for every new behavior.
Use SOLID to isolate code that changes often. Stop when the design becomes harder to understand than the problem.
A class design works well when each future change has a clear place to go.
If adding UPI payments requires edits in OrderService, InvoiceService, PaymentProcessor, and five tests, the design has poor change isolation.
If adding UPI payments means adding one UpiPaymentStrategy class and registering it, the design is easier to extend and safer to change.
SOLID helps answer five interview questions:
| Principle | Core Question | Failure Smell |
|---|---|---|
| SRP | “Why would this class change?” | One class handles unrelated business flows |
| OCP | “Can I add behavior without editing stable code?” | Large if/else or switch chains |
| LSP | “Can subclasses safely replace parents?” | Child class weakens or changes parent expectations |
| ISP | “Are clients forced to depend on unused methods?” | Interfaces contain methods some implementers cannot support |
| DIP | “Does high-level logic depend on low-level details?” | Services directly create concrete dependencies |
SRP means a class should have one reason to change.
Focus on the axis of change. Avoid judging SRP by method count alone.
A common interview failure is a UserService that handles authentication, profile updates, and email notifications.
Those responsibilities change for different reasons:
UserService looks convenient at first. It becomes painful when the interviewer says, “Now support OTP login,” or “Emails should move to a queue,” because unrelated changes touch the same class.
Each class now has a clear reason to change.
Look for SRP when one class name ends in Manager, Processor, or Service and contains verbs from different domains.
Do not split every method into a separate class.
A class with three methods can still have one responsibility if all methods belong to the same business capability.
In an e-commerce system, OrderService may coordinate order placement.
Payment charging, inventory reservation, and notification delivery should usually live behind separate collaborators.
OCP means code should accept new behavior through new code instead of repeated edits to stable code.
The most common OCP violation in LLD interviews is a growing if/else chain.
1class PaymentProcessor {
2 void pay(String type, int amount) {
3 if (type.equals("CARD")) {
4 System.out.println("Paid by card: " + amount);
5 } else if (type.equals("UPI")) {
6 System.out.println("Paid by UPI: " + amount);
7 } else if (type.equals("WALLET")) {
8 System.out.println("Paid by wallet: " + amount);
9 }
10 }
11}Every new payment method forces an edit in PaymentProcessor.
Repeated edits to stable code increase regression risk.
The Strategy pattern makes the payment behavior replaceable.
PaymentProcessor depends on PaymentStrategy, so it does not need to know every payment type.
Look for OCP when the interviewer says, “More types will be added later,” or when you are about to write if type == ....
Do not create strategies for logic that is unlikely to vary.
If there are exactly two hard-coded cases and no expected growth, a conditional can be clearer.
Payment gateways, pricing rules, shipping cost calculation, notification channels, and file exporters are strong OCP candidates.
LSP means a subtype must be safe to use wherever its parent type is expected.
A child class breaks LSP when it changes behavior that callers rely on.
The classic violation is Square extends Rectangle.
Mathematically, a square is a rectangle. In software, a mutable Rectangle usually promises that width and height can change independently. A Square cannot keep that promise.
If test code does this:
1resize rectangle width to 5
2resize rectangle height to 10
3expect area = 50A square may return 100 because setting height also changes width.
The subtype changed the parent’s expected behavior.
The same two calls give area 50 on a Rectangle and 100 on a Square, so Square cannot substitute for Rectangle. A shared Shape interface fixes it.
Use a shared abstraction that only promises behavior both shapes can support.
Look for LSP when inheritance feels natural in English and behavior differs in code.
Do not force inheritance because two classes share fields.
Use composition or a smaller interface when callers cannot safely substitute one type for the other.
A ReadOnlyFile should not extend WritableFile if callers expect write() to work.
ISP means clients should depend only on methods they use.
Large interfaces create fake obligations.
A bad interface is MultiFunctionDevice with print(), scan(), and fax().
A cheap printer can print. It cannot scan or fax.
If BasicPrinter implements MultiFunctionDevice, it must throw errors or provide empty methods for unsupported features. Both choices signal a bad interface.
Each device implements only the capabilities it supports.
Look for ISP when implementers throw UnsupportedOperationException, return dummy values, or leave methods empty.
Do not split interfaces so aggressively that every method gets its own interface by default.
Split by client need and capability boundary.
A restaurant system may have DineInOrder, DeliveryOrder, and PickupOrder.
Do not force all orders to implement assignDeliveryPartner().
DIP means high-level business logic should depend on interfaces for behavior that can vary.
Constructor injection is the default way to pass those dependencies.
Bad design:
1class OrderService {
2 private final StripeGateway gateway = new StripeGateway();
3}OrderService is hard to test and hard to extend.
If the interviewer says “support Razorpay too,” you must edit OrderService.
Better design:
1class OrderService {
2 private final PaymentGateway gateway;
3
4 OrderService(PaymentGateway gateway) {
5 this.gateway = gateway;
6 }
7}OrderService now works with Stripe, Razorpay, or a fake gateway used in tests.
Look for DIP when a service directly creates another concrete service using new.
Do not create interfaces for stable value objects like Money, Address, or Email.
Create interfaces only for behavior that varies.
OrderService should depend on PaymentGateway, InventoryService, and NotificationSender abstractions when those integrations can vary.
1interface PaymentGateway {
2 void pay(int amount);
3}
4
5class StripeGateway implements PaymentGateway {
6 @Override
7 public void pay(int amount) {
8 System.out.println("Stripe charged: " + amount);
9 }
10}
11
12class RazorpayGateway implements PaymentGateway {
13 @Override
14 public void pay(int amount) {
15 System.out.println("Razorpay charged: " + amount);
16 }
17}
18
19class OrderService {
20 private final PaymentGateway gateway;
21
22 public OrderService(PaymentGateway gateway) {
23 this.gateway = gateway;
24 }
25
26 public void placeOrder(int amount) {
27 gateway.pay(amount);
28 System.out.println("Order placed");
29 }
30}
31
32public class Main {
33 public static void main(String[] args) {
34 OrderService service = new OrderService(new StripeGateway());
35 service.placeOrder(500);
36 }
37}OrderProcessorA 150-line OrderProcessor often appears after 20 minutes of coding.
It starts small, then grows to validate orders, calculate discounts, charge payments, update inventory, send emails, and handle retries.
OrderProcessor violates SRP because validation, discounts, payments, inventory, and notifications change for different reasons.
It violates OCP when payment and notification types are controlled by conditionals.
| Step | Action | Principle |
|---|---|---|
| 1 | Keep OrderService as the coordinator only | SRP |
| 2 | Extract PaymentGateway for payment behavior | OCP + DIP |
| 3 | Extract InventoryService for stock changes | SRP |
| 4 | Extract NotificationSender for email/SMS/push | OCP |
| 5 | Inject dependencies through the constructor | DIP |
One 150-line OrderProcessor is split into a coordinator plus focused collaborators behind interfaces, injected through the constructor.
OrderService stays responsible for orchestration.
Specialized behavior moves into focused collaborators.
| Situation | Pick | Reason |
|---|---|---|
| One class has unrelated business responsibilities | Split by axis of change | Applies SRP |
You see if/else by type and more types are expected | Strategy interface | Applies OCP |
| A subclass must disable or alter parent behavior | Avoid inheritance | Protects LSP |
| Implementers have unsupported methods | Split the interface | Applies ISP |
| Service creates concrete dependencies internally | Constructor injection | Applies DIP and improves testability |
Over-engineering is worse than a mild SRP violation.
If your design has ten interfaces before you have two implementations, the interviewer will question whether you understand tradeoffs.
Introduce an abstraction when the problem has one of these signals:
Otherwise, keep the class concrete and simple.
A good interview design gives the next likely requirement an obvious, low-risk place to go.
Default: Start with simple concrete classes. Introduce interfaces at behavior boundaries that are likely to vary: payments, notifications, pricing, storage, external integrations, and policies. Use constructor injection by default. Use Strategy to replace growing
if/elsechains. Do not create interfaces for every class.
Warning: Candidates fail SOLID interviews in predictable ways:
- They know the acronym and cannot point to the exact class that should change.
- They treat SRP as “one method per class,” which creates noise.
- They claim every class needs an interface, including stable value objects.
- They use inheritance because names sound related, then violate LSP.
- They write
UnsupportedOperationExceptioninstead of splitting interfaces.- They directly instantiate dependencies inside services, making testing and extension harder.
- They over-abstract too early and make the design harder to explain under pressure.
No related case studies for this subpattern.