Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
What is SOLID?
SOLID is an acronym for five design principles that help you write maintainable, flexible, and testable code. Coined by Robert C. Martin (Uncle Bob) in the early 2000s, these principles distill decades of software engineering experience into actionable rules. Think of SOLID as guardrails on a mountain road — they don’t tell you where to drive, but they keep you from going off a cliff. The real power of SOLID is not in rigid adherence, but in recognizing when a violation is causing pain and knowing exactly which principle to apply as the remedy.| Letter | Principle | One-Liner |
|---|---|---|
| S | Single Responsibility | One class, one reason to change |
| O | Open/Closed | Open for extension, closed for modification |
| L | Liskov Substitution | Subtypes must be substitutable for base types |
| I | Interface Segregation | Many specific interfaces > one general interface |
| D | Dependency Inversion | Depend on abstractions, not concretions |
🚨 Code Smell Recognition
Before diving into principles, learn to recognize when code violates SOLID:- S Violations
- O Violations
- L Violations
- I Violations
- D Violations
Case Study: E-commerce Order System
We’ll refactor a poorly designed order system step by step.❌ Initial Bad Design (Violates ALL SOLID Principles)
- One class does everything (violates SRP)
- Hard to test (depends on external services)
- Can’t change payment/email without modifying Order
- Tightly coupled to implementations
S - Single Responsibility Principle
Each class should have only one reason to change.The keyword here is “reason to change,” not “one thing to do.” A
PricingService might have multiple methods (calculate discount, apply tax, convert currency), but they all change for the same reason: pricing rules change. If your class changes when the email template changes and when the database schema changes, those are two different reasons — and a sign you should split it. In practice, ask yourself: “If I hand this class to a single team to own, would they have conflicts with other teams?” If yes, SRP is being violated.
✅ Refactored: Separate Responsibilities
O - Open/Closed Principle
Open for extension, closed for modification.This principle is the reason design patterns exist. The goal: when a new requirement arrives (a new discount type, a new payment method, a new notification channel), you should be able to add it by writing new code, not by editing existing, tested code. The mechanism is almost always the same — define an abstraction (interface or abstract class), then let new implementations plug in. When you see a growing if/elif chain based on type, that is your cue: OCP is being violated, and a Strategy or Factory pattern is the fix.
✅ Extensible Discount System
L - Liskov Substitution Principle
Subtypes must be substitutable for their base types.Named after Barbara Liskov, this principle answers a simple question: can you replace a parent object with a child object and have everything still work correctly? If not, your inheritance hierarchy is lying about its contracts. The classic violation is the Penguin-extends-Bird problem (penguins cannot fly), but the real-world violations are subtler: a
ReadOnlyDatabase that inherits from Database but throws on write(), or a Square that inherits from Rectangle but silently changes both dimensions when you set one. The fix is almost always to redesign the hierarchy or use composition.
❌ Violation Example
✅ Correct Hierarchy
✅ Payment Example
I - Interface Segregation Principle
Clients should not depend on interfaces they don’t use.If you have ever seen a class that implements an interface but stubs out half the methods with
pass or raise NotImplementedError, you have witnessed ISP being violated. The fix is to split the fat interface into smaller, role-specific ones. A Printer interface should not force implementors to also support scanning, faxing, and stapling. This principle works hand-in-hand with SRP: just as classes should have a single responsibility, interfaces should have a single purpose. The practical benefit is decoupling — when you depend on a narrow interface, changes to unrelated capabilities cannot break your code.
❌ Fat Interface
✅ Segregated Interfaces
✅ E-commerce Example
D - Dependency Inversion Principle
Depend on abstractions, not concretions.This is the principle that makes everything testable. When your
OrderService directly creates a PostgreSQLDatabase inside its constructor, you cannot test the order logic without a running database. But when it accepts a Database interface through its constructor (dependency injection), you can pass in a MockDatabase for tests, a PostgreSQL for production, and a SQLite for local development. The “inversion” is about who controls the dependency — instead of the class deciding what it uses, the caller decides what to inject. This seemingly small shift transforms tightly coupled monoliths into flexible, testable, swappable architectures.
❌ Tight Coupling
✅ Dependency Injection
SOLID Summary
| Principle | One-liner | Benefit |
|---|---|---|
| Single Responsibility | One reason to change | Easier maintenance |
| Open/Closed | Extend without modifying | Safer changes |
| Liskov Substitution | Subtypes are interchangeable | Reliable polymorphism |
| Interface Segregation | Small, focused interfaces | Less coupling |
| Dependency Inversion | Depend on abstractions | Flexible, testable |
🎯 Quick Decision Guide
When designing classes, ask yourself:Single Responsibility Check
Open/Closed Check
Liskov Substitution Check
Interface Segregation Check
💡 Interview Tips
How to Demonstrate SOLID
How to Demonstrate SOLID
- “I’m separating payment processing into its own class for Single Responsibility”
- “Using an interface here so we can add new payment methods without modifying existing code - that’s Open/Closed”
- “I’ll inject the database as a dependency so it’s easy to test”
When SOLID is Overkill
When SOLID is Overkill
- Simple scripts or one-off utilities
- Classes with only 1-2 methods
- Early prototypes (refactor later)
- Multiple developers
- Long-term maintenance
- Frequent changes/extensions
Testing Benefits
Testing Benefits
Interview Questions
The initial bad Order class has 6 methods covering payment, inventory, email, PDF, and database. A junior developer proposes splitting it into exactly 6 classes -- one per method. Is that the right level of granularity for SRP? How do you decide where to draw the line?
The initial bad Order class has 6 methods covering payment, inventory, email, PDF, and database. A junior developer proposes splitting it into exactly 6 classes -- one per method. Is that the right level of granularity for SRP? How do you decide where to draw the line?
- One class per method is too granular — that is not what SRP means. SRP says “one reason to change,” not “one method.” A PricingService with
calculate_discount(),apply_tax(), andconvert_currency()has three methods but one reason to change: pricing rules evolve. Splitting those into three classes would create unnecessary indirection with no design benefit. - The right question to ask is: “Who would request a change to this code?” If the finance team changes tax rules, that affects PricingService. If the marketing team changes discount logic, that also affects PricingService. If those two teams are the same stakeholder (or always change the rules together), one class is fine. If they are different teams with different release cadences, split them.
- Robert Martin’s original formulation is actually “a class should have only one reason to change,” which he later refined to “a module should be responsible to one, and only one, actor.” The actor framing is more practical than counting methods.
- In the e-commerce case, the right split is roughly: Order (data and order-level calculations), PricingService (discounts and pricing rules), PaymentProcessor (payment gateway interaction), NotificationService (email/SMS), InventoryService (stock management), OrderRepository (persistence). That is 6 classes, but the split is by responsibility domain, not by method count.
- You have a UserService with
create_user(),update_profile(),change_password(), anddelete_user(). A colleague says this violates SRP because it has four methods. How do you respond? - At what point does splitting classes for SRP become over-engineering? Can you give a concrete example where you chose NOT to split despite a mild SRP tension?
The DiscountRule system uses OCP beautifully -- adding a new discount is just a new class. But what happens when the business says: 'Discounts should not stack -- only the best one applies' or 'VIP discount must always apply on top of any other discount'? Does your OCP design survive this requirement?
The DiscountRule system uses OCP beautifully -- adding a new discount is just a new class. But what happens when the business says: 'Discounts should not stack -- only the best one applies' or 'VIP discount must always apply on top of any other discount'? Does your OCP design survive this requirement?
- This is where the real-world stress-tests OCP. The current PricingService iterates through all rules sequentially, applying each one. That model breaks under both new requirements because the interaction between rules is now a concern, and that logic lives in PricingService, not in individual rules.
- For “best discount only”: the PricingService needs to evaluate all applicable rules, collect the resulting prices, and pick the minimum. Each DiscountRule still computes independently (OCP preserved for individual rules), but the composition strategy changes. I would extract the composition logic into a
DiscountStrategyinterface with implementations likeStackAllDiscounts,BestDiscountOnly, andPriorityStackDiscounts. - For “VIP always applies on top”: you need rule priority and layering. I would add a
priority()method to DiscountRule and ais_stackable()boolean. The composition strategy sorts by priority, applies non-stackable rules to find the best base discount, then applies stackable rules on top. The DiscountRule interface grows by one or two methods, but existing implementations only need minor additions. - The key insight: OCP is preserved at the rule level (new discount types do not modify existing rules), but the composition mechanism needs modification. This is expected — the principle says “closed for modification” at the right abstraction level, not at every level.
- If you have 20 discount rules with complex priority and stacking logic, how do you test that the composition strategy is correct? What testing approach gives you the most confidence?
- The business now says “show the customer which discount was applied and why.” How does this affect your DiscountRule interface?
The LSP section shows the Bird/Penguin violation. But here is a harder case: you have a Rectangle class with setWidth() and setHeight(). Someone creates Square that extends Rectangle and overrides both setters to keep width equal to height. Does Square violate LSP?
The LSP section shows the Bird/Penguin violation. But here is a harder case: you have a Rectangle class with setWidth() and setHeight(). Someone creates Square that extends Rectangle and overrides both setters to keep width equal to height. Does Square violate LSP?
- Yes, Square violates LSP, and this is one of the most famous examples in software design history. The violation is subtle because Square IS-A Rectangle in mathematics, but not in code. The issue is behavioral contracts.
- Consider code that uses a Rectangle:
r.setWidth(5); r.setHeight(10); assert r.area() == 50. This is a reasonable postcondition for Rectangle. But ifris actually a Square,setHeight(10)also sets width to 10, sor.area() == 100. The assertion fails. Square cannot be substituted for Rectangle without breaking existing code’s assumptions. - The root cause is that Rectangle has an implicit contract: width and height are independently mutable. Square violates this contract by coupling them. The type signature is preserved, but the behavioral contract is broken.
- Solutions: Make both classes immutable — without setters, the behavioral contract issue disappears. Or do not use inheritance at all — Square is not a subtype of Rectangle in code, it is a special case. Use a factory method
Rectangle.create_square(side)that returns a Rectangle with equal dimensions.
- If immutability solves the Square/Rectangle problem, does that mean LSP is only relevant for mutable objects? Can you think of an LSP violation with immutable types?
- Java’s
Collections.unmodifiableList()returns a List that throwsUnsupportedOperationExceptiononadd(). Is this an LSP violation?
In the ISP section, Robot only implements Workable. But what if the system needs to track ALL entities in a unified list for scheduling? You need a common type. How do you reconcile ISP with the need for a shared interface?
In the ISP section, Robot only implements Workable. But what if the system needs to track ALL entities in a unified list for scheduling? You need a common type. How do you reconcile ISP with the need for a shared interface?
The DIP example injects Database, PaymentGateway, and EmailService into OrderService. In production with 20+ services, who creates the concrete implementations and wires them together? Walk me through the real dependency injection wiring.
The DIP example injects Database, PaymentGateway, and EmailService into OrderService. In production with 20+ services, who creates the concrete implementations and wires them together? Walk me through the real dependency injection wiring.
- In a small app, you wire it manually in
main():db = PostgreSQLDatabase(); service = OrderService(db). But at 20+ services with cross-cutting dependencies, manual wiring becomes a maintenance nightmare. - In Python, the common approaches are: (1) A composition root — a single place (usually
main.pyorapp_factory()) that creates all dependencies and wires them. This is explicit and debuggable but verbose. (2) A DI container likedependency-injectorthat automates wiring based on type hints. You register bindings (Database -> PostgreSQLDatabase) and the container resolves the graph. (3) Framework-level DI like FastAPI’sDepends(). - The anti-pattern to avoid is the Service Locator, where dependencies are looked up from a global registry:
ServiceLocator.get(Database). This hides dependencies, makes testing harder, and turns compile-time errors into runtime errors. DI makes dependencies explicit in the constructor; Service Locator buries them inside method bodies. - At scale (Uber, Stripe), the composition root approach is common. A service starts, reads config, constructs concrete implementations in the right order, and injects them. The wiring code is boring — and boring is a feature. Boring code is predictable code.
- What is the difference between Dependency Injection and Dependency Inversion? They sound similar but are separate concepts.
- You need PostgreSQL in production, SQLite in development, and MockDB in tests. How do you configure this without if/elif chains?
A developer says: 'The original 1-class Order was 30 lines. Now we have 8 classes and 150 lines for the same behavior. YAGNI says we should not add complexity we do not need yet.' How do you respond?
A developer says: 'The original 1-class Order was 30 lines. Now we have 8 classes and 150 lines for the same behavior. YAGNI says we should not add complexity we do not need yet.' How do you respond?
- They have a valid point, and this is one of the most important judgment calls in engineering. SOLID and YAGNI are in genuine tension. Applying SOLID to a hackathon project is over-engineering. Applying it to core order processing that 50 engineers maintain for 5 years is essential.
- The heuristic I use: “How many people will touch this code, and how long will it live?” If it is one person for a week, the monolithic class is fine. If it is a team for years, the SOLID refactoring pays for itself within the first few feature additions. The 150 lines of well-separated code is cheaper to maintain than 30 lines of tangled code once you add the 5th payment method.
- The practical compromise is “just-in-time SOLID”: start with the simpler design and refactor when you feel the first pain. When adding a second payment method requires modifying the Order class, that is the signal to extract PaymentProcessor. Martin Fowler calls this the Rule of Three: first time just do it, second time wince, third time refactor.
- Over-engineering is as real a problem as under-engineering. The skill is knowing which one you are closer to, and that requires experience and context about the codebase’s trajectory.
- You are a tech lead on a new project. How do you communicate to your team when to apply SOLID strictly versus when to be pragmatic?
- Can you give a real example where premature SOLID application caused more harm than the problem it was trying to prevent?
Look at all 5 SOLID principles together. Which two have the strongest synergy, and which two are most likely to conflict in practice?
Look at all 5 SOLID principles together. Which two have the strongest synergy, and which two are most likely to conflict in practice?
- Strongest synergy: OCP and DIP. You cannot truly achieve OCP without DIP. If your high-level module depends on a concrete class, “extending” means modifying that class. DIP provides the abstraction layer that OCP extends through. In the e-commerce example, the DiscountRule abstraction (DIP) enables adding new discount types without modifying PricingService (OCP).
- Most likely to conflict: SRP and ISP. When you aggressively apply ISP, you create many tiny interfaces. Classes implementing them become narrowly focused, sometimes holding almost no logic — just glue code. Conversely, applying SRP can create classes implementing 5 narrow interfaces, raising the question: is a class with 5 interface contracts really “single responsibility”?
- Another tension: SRP and pragmatism. Having 40 single-method classes is worse than 10 four-method classes if all four methods change for the same reason. SRP at the service level matters more than SRP at the method level.
- The resolution is context-dependent. In a microservice with 3 endpoints, SRP at the class level matters less than SRP at the service level. In a monolith with 500 classes, ISP becomes critical. SOLID principles are not equally weighted in every situation.
- LSP and OCP both deal with subtyping and extension. Can you give an example where satisfying one forces you to violate the other?
- If you could only teach a junior developer TWO of the five SOLID principles, which two would give them the most leverage?
The DIP test example creates MockDatabase, MockPayment, and MockEmail. A QA engineer argues that mocking everything makes the tests meaningless. Who is right?
The DIP test example creates MockDatabase, MockPayment, and MockEmail. A QA engineer argues that mocking everything makes the tests meaningless. Who is right?
- Both sides have merit. Unit tests with mocks verify that OrderService’s logic is correct in isolation: does it call
charge()beforesave()? Does it handle payment failure? These run in milliseconds and catch logic bugs. Integration tests with real dependencies verify that pieces actually fit together: does the SQL work against PostgreSQL? Does Stripe accept the payload format? - The danger the QA engineer points to is real: over-mocking. If your mock always returns
success=Trueand Stripe rejects 5% of charges due to expired cards, your tests give false confidence. The rule I follow: mock at the boundary of YOUR code. Mock external APIs (Stripe, SendGrid) because you do not control them, but use real implementations for your own classes. - The Testing Pyramid resolves this: fast unit tests with mocks on every commit (30 seconds), integration tests against sandboxed services nightly, and contract tests (Pact) to verify mocks match real API behavior. Mocks and integration tests are not alternatives — they are different layers of the same safety net.
- At Shopify and Stripe, the pattern is: unit test the business logic with mocks, integration test the boundaries with real dependencies, contract test the mock fidelity. Each layer catches different classes of bugs.
- Your MockDatabase always returns success. In production, PostgreSQL throws a unique constraint violation on duplicate order IDs. How do you catch this without a full PostgreSQL in tests?
- What are contract tests (like Pact), and how do they solve the “mocks drift from reality” problem?
Interview Deep-Dive
Walk through the e-commerce refactoring in this case study. If you had to apply only TWO of the five SOLID principles to get the biggest improvement, which two would you pick and why?
Walk through the e-commerce refactoring in this case study. If you had to apply only TWO of the five SOLID principles to get the biggest improvement, which two would you pick and why?
- I would pick Single Responsibility (S) and Dependency Inversion (D) as the two highest-impact principles.
- SRP is first because the original Order class has five responsibilities: data management, pricing, payment processing, inventory updates, email sending, and PDF generation. Extracting each into its own class immediately makes the codebase navigable, testable, and assignable to different team members. This single refactoring eliminates the God class anti-pattern.
- DIP is second because once you have separated the responsibilities, you need those services to depend on abstractions rather than concrete implementations. Injecting a PaymentGateway interface rather than hardcoding StripePayment means you can test the OrderService without a live Stripe connection, swap to PayPal for a different market, or use a MockPayment in CI/CD pipelines.
- OCP, LSP, and ISP would naturally follow once S and D are in place. With abstractions injected (DIP), adding new payment methods becomes a matter of creating new classes (OCP). With focused interfaces from the extraction (SRP), you avoid fat interfaces (ISP). And with proper abstractions, subtypes stay substitutable (LSP).
- In my experience, SRP and DIP together give you roughly eighty percent of the maintainability benefit of SOLID.
The case study shows an OCP-compliant discount system with DiscountRule implementations. An interviewer asks: 'What if we need to apply multiple discounts with a priority order, and some discounts are mutually exclusive?' How does your design handle that?
The case study shows an OCP-compliant discount system with DiscountRule implementations. An interviewer asks: 'What if we need to apply multiple discounts with a priority order, and some discounts are mutually exclusive?' How does your design handle that?
- The current design passes a list of DiscountRules to the PricingService and applies them sequentially. To handle priority and mutual exclusivity, I would add two concepts: a priority field on each rule, and a discount group for mutual exclusivity.
- Each DiscountRule gets a priority (integer, lower is higher priority) and an optional group name. The PricingService sorts rules by priority, then within each group, applies only the best (highest-saving) rule and skips the rest in that group. Rules without a group are applied independently.
- This follows OCP because adding a new discount type still means creating a new DiscountRule class. The prioritization and exclusivity logic lives in the PricingService, which iterates over any list of rules without knowing their specific types.
- For example: “HolidayDiscount” and “LoyaltyDiscount” might be in different groups (both apply), but “FirstOrderDiscount” and “ReferralDiscount” might be in the same group “new_customer” (only the better one applies).
- An alternative is the Chain of Responsibility pattern, where each rule decides whether to apply itself and whether to pass control to the next rule. This gives even more flexibility for complex discount logic.
In the LSP section, the Bird/Penguin problem is shown. An interviewer asks: 'We have a real codebase with 200 classes that inherit from a base class with a method that some subclasses cannot support. We cannot rewrite everything. What is a practical migration path?'
In the LSP section, the Bird/Penguin problem is shown. An interviewer asks: 'We have a real codebase with 200 classes that inherit from a base class with a method that some subclasses cannot support. We cannot rewrite everything. What is a practical migration path?'
- In a real codebase with 200 subclasses, a complete rewrite is not feasible. I would use a phased approach.
- Phase 1: Introduce the new interfaces (FlyingBird, SwimmingBird) alongside the existing Bird class. The existing Bird class implements all the new interfaces to maintain backward compatibility. No existing code breaks.
- Phase 2: Gradually migrate consumers. When a function currently takes Bird but only calls fly(), change its parameter type to FlyingBird. This is safe because Bird implements FlyingBird, so all existing callers still work. But now the type system prevents passing a Penguin to a function that expects flying behavior.
- Phase 3: Migrate subclasses one at a time. Change Penguin from extending Bird to extending SwimmingBird. If some code still passes Penguin where Bird is expected, the type checker flags it.
- Phase 4: Once all subclasses are migrated, deprecate the catch-all Bird class and remove the problematic fly() method from the base.
- This is the “Strangler Fig” pattern applied to a class hierarchy. Each phase is independently deployable and testable. The key is never breaking existing callers during the migration.
The Dependency Inversion section shows constructor injection. An interviewer asks: 'What are the trade-offs between constructor injection, setter injection, and method injection? When would you use each?'
The Dependency Inversion section shows constructor injection. An interviewer asks: 'What are the trade-offs between constructor injection, setter injection, and method injection? When would you use each?'
- Constructor injection is the default and best choice for required dependencies. The object cannot be created in an invalid state because all dependencies are provided at construction time. It makes dependencies explicit — you can look at the constructor signature and immediately see what the class needs. It also works naturally with immutability: once set, the dependencies do not change.
- Setter injection is for optional dependencies or dependencies that might change after construction. For example, a Logger that can be swapped at runtime, or a CacheProvider that might be configured after the service is created. The downside is that the object can exist in a partially configured state — you must handle the case where the setter was never called.
- Method injection is for dependencies that are only needed for a single operation, not for the lifetime of the object. For example, passing an AuditLogger to a specific processPayment() call because only that method needs auditing. This keeps the class lightweight and avoids storing dependencies that are rarely used.
- In practice, I use constructor injection for 90% of cases, method injection for 8%, and setter injection for 2%. If I find myself using setter injection frequently, it usually means my class has too many optional behaviors and should be split.