Applying SOLID and Composition in LLD Problems
Fast recall
Quick revision
Core answer
Vehicle has a EngineType instead of ElectricCar extends Car. Inheritance for true 'is-a' relationships only; composition for behavior variation. The result: classes are small, swappable, testable in isolation, and extensible without breaking existing code.Whether you apply SOLID as design forces (not as recited bullet points), and whether you reach for composition before inheritance for behavior variation.
Treating SOLID as a five-item to-do list to mention; using inheritance as the default tool for 'reuse' (creating fragile hierarchies).
Show concrete refactors from violation → fix; talk about LSP via the Rectangle/Square problem; show composition replacing a 4-level hierarchy with a 2-level + interface.
Interview-ready guide
Interview answer
Why SOLID and composition matter
Most LLD anti-patterns come from two failures:
- 1Mixing responsibilities — one class does too much, changes ripple unpredictably (violates SRP, OCP)
- 2Inheriting for code reuse — deep extends chains, subclasses can't substitute parents (violates LSP, makes change risky)
SOLID is the named set of constraints that prevent these. Composition over inheritance is the structural choice making them easy to honor. Result: classes small, swappable, extensible, local in change.
Interviewer doesn't want to hear 'I will follow SOLID.' Wants to see decisions consistent with it as you go.
S — SRP
One class, one reason to change.
Violation: Order class with place(), calculateTax(), sendConfirmationEmail(), persistToDb(), exportToCsv(). Four different reasons to change.
Fix: Order (state + place/cancel), TaxCalculator, OrderConfirmationMailer, OrderRepository, OrderExporter. Each changes for one reason. Use case orchestrates.
O — OCP
Open for extension, closed for modification. Mechanism: interface + new implementation.
Violation: PaymentProcessor.pay() with if/else if for credit_card, paypal, upi, crypto. Every new method edits existing code.
Fix: PaymentMethod interface. CreditCardPayment, PaypalPayment, UpiPayment implement it. New method = new file, never edit existing. Critical when existing code has dozens of dependents and tests.
L — LSP
Subclasses must honor parent's contract.
Classic violation: Square extends Rectangle. setWidth forces height; setHeight forces width. resize(Rectangle r) that sets width=5, height=10 expects area=50 — fails for Square (100).
Fix: don't model square as rectangle. Both implement Shape with area().
Practical heuristic: subclass overrides to throw UnsupportedOperationException or behaves differently from documented expectations → LSP violated; inheritance is wrong.
I — ISP
Don't force clients to depend on methods they don't use. Many small role-specific interfaces beat one fat interface.
Violation: Worker interface with work(), eat(), sleep(). Robot.eat() throws UnsupportedOperationException.
Fix: split by role. Workable, Eatable, Sleepable. Human implements all three; Robot implements Workable only.
LLD application: keep repository/gateway/strategy interfaces focused. UserRepository with 30 methods hard to mock; signals some methods belong elsewhere. UserFinder, UserUpdater, UserDeleter often cleaner.
D — DIP
Depend on abstractions, not concretions.
Violation: OrderService with private MySqlOrderRepository = new MySqlOrderRepository(). Can't unit-test without MySQL. Can't swap DB. Can't decorate mailer with retries.
Fix: OrderRepository + EmailSender interfaces. OrderService constructor takes them. Adapters implement them. Tests pass fakes. Production wires real ones via Spring.
Constructor injection is canonical Java mechanism. Avoid field injection (hides dependencies, breaks immutability).
Composition over inheritance — the bigger lever
Inheritance is the heaviest coupling tool in OO. Subclass inherits everything. Reaching for extends should be deliberate.
Inheritance appropriate when: true is-a satisfying LSP, subclass substitutable everywhere parent is, behavior genuinely shared and stable (AbstractList → ArrayList).
Composition appropriate when: adding behavior variation, swapping at runtime, combining behaviors.
Inheritance trap: Vehicle ← Car ← ElectricCar ← LuxuryElectricCar ← AutonomousLuxuryElectricCar. Four levels deep. Autonomous but not electric? Can't combine traits.
Composition fix: Vehicle has EngineType, ComfortLevel, AutonomyLevel. Three independent variation axes. Any combination expressible. New axis = one more field, not hierarchy explosion.
For behavioral composition: DrivetrainBehavior + ParkingBehavior interfaces. Swappable at construction time. Easy to test in isolation.
Common mistakes
- SOLID as recitation — mentioning names without showing design force
- Inheritance as default — extends for reuse rather than true is-a
- Deep hierarchies — more than 2-3 levels almost always a smell
- Subclasses overriding to throw UnsupportedOperationException — LSP red flag
- God interfaces — UserService with 25 methods; clients use 3
- Field injection — hides dependencies, breaks immutability, harder to test
- 'I'll add an interface later if needed' — usually you won't; concrete dependency leaks everywhere
- Interfaces for everything — YAGNI; add when second implementation appears
- OCP taken too literally — sometimes editing existing code is right; OCP is guideline, not religion
Recommendation
Apply SOLID as design forces, not as a checklist. Single responsibility per class; new behavior via new implementations of stable interfaces (Strategy is the workhorse); subclasses that genuinely substitute their parents (otherwise prefer composition); small role-specific interfaces; depend on abstractions and constructor-inject concrete adapters. Default to composition for behavior variation; reserve inheritance for true is-a relationships that pass the Liskov test. YAGNI moderates everything — don't introduce abstraction without a concrete second use. The result is small, swappable, testable, and extensible classes — which is what an interviewer is grading.
Applying SOLID and Composition in LLD Problems
Direct Answer
SOLID isn't a checklist to recite — it's a set of forces that shape good OO design. In real LLD problems: keep classes single-purpose (SRP); allow new behavior without modifying existing classes via interfaces and Strategy/Factory (OCP); ensure subclasses honor their supertype's contract (LSP); split fat interfaces into role-specific ones (ISP); depend on abstractions not concrete classes (DIP). Prefer composition over inheritance — a Vehicle has a EngineType instead of ElectricCar extends Car. Inheritance for true 'is-a' relationships only; composition for behavior variation. The result: classes are small, swappable, testable in isolation, and extensible without breaking existing code.
Common Mistakes & Pitfalls
Treating SOLID as a five-item to-do list to mention; using inheritance as the default tool for 'reuse' (creating fragile hierarchies).
Check your understanding
Answer these without looking back at the article.
- Refactor a 25-method `UserService` god class into ISP-compliant pieces.
- Show how Strategy + DIP gives you a payment system that's extensible without edits.
- When is inheritance the right tool, and what test do you apply?
- How does YAGNI temper SOLID — give an example of over-engineering.
Related interview prep categories
1Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.