> ## 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.

# SOLID Principles in Practice

> Applying SOLID principles to real-world LLD scenarios

<Frame>
  <img src="https://mintcdn.com/devweeekends/sTu6A4whRFPJo0_g/images/LLD/solid-principles.svg?fit=max&auto=format&n=sTu6A4whRFPJo0_g&q=85&s=853b195401a06c650dd00eb9b17da962" alt="SOLID Principles" width="1080" height="1080" data-path="images/LLD/solid-principles.svg" />
</Frame>

## 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:

<Tabs>
  <Tab title="S Violations">
    ```python theme={null}
    # 🚨 GOD CLASS - does everything
    class UserManager:
        def create_user(self): pass
        def authenticate(self): pass
        def send_email(self): pass      # ❌ Email is not user management
        def generate_report(self): pass  # ❌ Reporting is separate concern
        def backup_data(self): pass      # ❌ Backup is infrastructure

    # Signs of SRP violation:
    # - Class has many unrelated methods
    # - Class name includes "Manager", "Handler", "Processor", "Utils"
    # - You need to change the class for different reasons
    ```
  </Tab>

  <Tab title="O Violations">
    ```python theme={null}
    # 🚨 MODIFICATION REQUIRED for new types
    class DiscountCalculator:
        def calculate(self, customer_type: str, amount: float):
            if customer_type == "regular":
                return amount * 0.95
            elif customer_type == "premium":
                return amount * 0.90
            elif customer_type == "vip":      # ❌ Had to modify existing code
                return amount * 0.80
            # Adding new type = modifying this class

    # Signs of OCP violation:
    # - Adding new type requires modifying existing code
    # - Long if-elif chains based on type
    # - switch/case statements that grow over time
    ```
  </Tab>

  <Tab title="L Violations">
    ```python theme={null}
    # 🚨 SUBTYPE breaks parent contract
    class Bird:
        def fly(self):
            return "Flying"

    class Penguin(Bird):
        def fly(self):
            raise Exception("Can't fly!")  # ❌ Violates LSP

    # Using Bird anywhere would break with Penguin:
    def make_bird_fly(bird: Bird):
        return bird.fly()  # 💥 Crashes for Penguin!

    # Signs of LSP violation:
    # - Subclass throws "not implemented" exceptions
    # - Subclass has empty/no-op override methods
    # - Type checking required before calling methods
    ```
  </Tab>

  <Tab title="I Violations">
    ```python theme={null}
    # 🚨 FAT INTERFACE forces unused methods
    class IWorker(ABC):
        @abstractmethod
        def work(self): pass
        
        @abstractmethod
        def eat(self): pass
        
        @abstractmethod
        def sleep(self): pass

    class Robot(IWorker):
        def work(self): 
            print("Working")
        
        def eat(self):
            pass  # ❌ Robot doesn't eat - forced to implement
        
        def sleep(self):
            pass  # ❌ Robot doesn't sleep - forced to implement

    # Signs of ISP violation:
    # - Classes implement methods they don't use
    # - Empty/stub method implementations
    # - Changes to interface affect unrelated clients
    ```
  </Tab>

  <Tab title="D Violations">
    ```python theme={null}
    # 🚨 TIGHT COUPLING to concrete classes
    class OrderService:
        def __init__(self):
            self.db = MySQLDatabase()       # ❌ Concrete dependency
            self.email = SMTPEmailSender()  # ❌ Hard to test
            self.payment = StripePayment()  # ❌ Hard to swap
        
        def place_order(self, order):
            self.db.save(order)
            self.email.send(order.customer_email)
            self.payment.charge(order.total)

    # Signs of DIP violation:
    # - Class creates its own dependencies with `new`/direct instantiation
    # - Hard to unit test (no way to inject mocks)
    # - Changing implementation requires modifying clients
    ```
  </Tab>
</Tabs>

***

## Case Study: E-commerce Order System

We'll refactor a poorly designed order system step by step.

### ❌ Initial Bad Design (Violates ALL SOLID Principles)

```python theme={null}
class Order:
    def __init__(self, items, customer_email):
        self.items = items
        self.customer_email = customer_email
        self.status = "pending"
    
    def calculate_total(self):
        total = sum(item.price * item.quantity for item in self.items)
        # Apply discount based on items
        if total > 100:
            total *= 0.9
        return total
    
    def process_payment(self, card_number, cvv):
        # Direct payment processing
        print(f"Processing payment of ${self.calculate_total()}")
        # Credit card API call here
        return True
    
    def update_inventory(self):
        for item in self.items:
            # Direct database call
            print(f"Reducing stock for {item.name}")
    
    def send_confirmation_email(self):
        # Direct email sending
        print(f"Sending email to {self.customer_email}")
    
    def generate_invoice_pdf(self):
        # PDF generation logic
        print("Generating PDF invoice")
    
    def save_to_database(self):
        # Database save logic
        print("Saving order to database")
```

**Problems**:

* 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

```python theme={null}
# Order only handles order data
class Order:
    def __init__(self, items: List[OrderItem], customer: Customer):
        self.id = uuid.uuid4()
        self.items = items
        self.customer = customer
        self.status = OrderStatus.PENDING
        self.created_at = datetime.now()
    
    def calculate_total(self) -> Decimal:
        return sum(item.subtotal for item in self.items)

# Separate class for pricing/discounts
class PricingService:
    def __init__(self, discount_rules: List[DiscountRule]):
        self.discount_rules = discount_rules
    
    def calculate_final_price(self, order: Order) -> Decimal:
        total = order.calculate_total()
        for rule in self.discount_rules:
            total = rule.apply(total, order)
        return total

# Separate class for payment
class PaymentProcessor:
    def process(self, order: Order, payment_method: PaymentMethod) -> PaymentResult:
        pass

# Separate class for notifications
class NotificationService:
    def send_order_confirmation(self, order: Order) -> None:
        pass

# Separate class for inventory
class InventoryService:
    def reserve_items(self, items: List[OrderItem]) -> bool:
        pass
    
    def release_items(self, items: List[OrderItem]) -> None:
        pass

# Separate class for persistence
class OrderRepository:
    def save(self, order: Order) -> None:
        pass
    
    def find_by_id(self, order_id: UUID) -> Optional[Order]:
        pass
```

***

## 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

```python theme={null}
from abc import ABC, abstractmethod

class DiscountRule(ABC):
    @abstractmethod
    def apply(self, total: Decimal, order: Order) -> Decimal:
        pass
    
    @abstractmethod
    def is_applicable(self, order: Order) -> bool:
        pass

class BulkDiscountRule(DiscountRule):
    """10% off for orders over $100"""
    def is_applicable(self, order: Order) -> bool:
        return order.calculate_total() > 100
    
    def apply(self, total: Decimal, order: Order) -> Decimal:
        if self.is_applicable(order):
            return total * Decimal("0.9")
        return total

class FirstOrderDiscountRule(DiscountRule):
    """15% off for first-time customers"""
    def __init__(self, order_repository: OrderRepository):
        self.order_repository = order_repository
    
    def is_applicable(self, order: Order) -> bool:
        past_orders = self.order_repository.find_by_customer(order.customer.id)
        return len(past_orders) == 0
    
    def apply(self, total: Decimal, order: Order) -> Decimal:
        if self.is_applicable(order):
            return total * Decimal("0.85")
        return total

class HolidayDiscountRule(DiscountRule):
    """20% off during holidays"""
    def __init__(self, holiday_calendar: HolidayCalendar):
        self.holiday_calendar = holiday_calendar
    
    def is_applicable(self, order: Order) -> bool:
        return self.holiday_calendar.is_holiday(order.created_at)
    
    def apply(self, total: Decimal, order: Order) -> Decimal:
        if self.is_applicable(order):
            return total * Decimal("0.8")
        return total

# Adding new discount = new class, no modification to existing code!
class LoyaltyDiscountRule(DiscountRule):
    """5% off for loyalty members"""
    def is_applicable(self, order: Order) -> bool:
        return order.customer.is_loyalty_member
    
    def apply(self, total: Decimal, order: Order) -> Decimal:
        if self.is_applicable(order):
            return total * Decimal("0.95")
        return total
```

***

## 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

```python theme={null}
class Bird:
    def fly(self):
        return "Flying"

class Penguin(Bird):
    def fly(self):
        raise Exception("Penguins can't fly!")  # Violates LSP!
```

### ✅ Correct Hierarchy

```python theme={null}
class Bird(ABC):
    @abstractmethod
    def move(self):
        pass

class FlyingBird(Bird):
    def move(self):
        return self.fly()
    
    def fly(self):
        return "Flying through the air"

class SwimmingBird(Bird):
    def move(self):
        return self.swim()
    
    def swim(self):
        return "Swimming in water"

class Sparrow(FlyingBird):
    pass

class Penguin(SwimmingBird):
    pass

# Now any Bird can be used interchangeably
def make_bird_move(bird: Bird):
    print(bird.move())  # Works for all birds!
```

### ✅ Payment Example

```python theme={null}
class PaymentMethod(ABC):
    @abstractmethod
    def pay(self, amount: Decimal) -> PaymentResult:
        pass
    
    @abstractmethod
    def supports_refund(self) -> bool:
        pass

class CreditCardPayment(PaymentMethod):
    def pay(self, amount: Decimal) -> PaymentResult:
        # Process credit card
        return PaymentResult(success=True)
    
    def supports_refund(self) -> bool:
        return True
    
    def refund(self, amount: Decimal) -> RefundResult:
        return RefundResult(success=True)

class CashOnDelivery(PaymentMethod):
    def pay(self, amount: Decimal) -> PaymentResult:
        # Mark as COD
        return PaymentResult(success=True, is_cod=True)
    
    def supports_refund(self) -> bool:
        return False  # No refund for COD until delivered

# Client code works with any PaymentMethod
def process_order(order: Order, payment: PaymentMethod):
    result = payment.pay(order.total)
    if result.success:
        order.mark_as_paid()
```

***

## 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

```python theme={null}
class Worker(ABC):
    @abstractmethod
    def work(self):
        pass
    
    @abstractmethod
    def eat(self):
        pass
    
    @abstractmethod
    def sleep(self):
        pass
    
    @abstractmethod
    def attend_meeting(self):
        pass
    
    @abstractmethod
    def write_report(self):
        pass
```

### ✅ Segregated Interfaces

```python theme={null}
class Workable(ABC):
    @abstractmethod
    def work(self):
        pass

class Feedable(ABC):
    @abstractmethod
    def eat(self):
        pass

class Sleepable(ABC):
    @abstractmethod
    def sleep(self):
        pass

class Manageable(ABC):
    @abstractmethod
    def attend_meeting(self):
        pass
    
    @abstractmethod
    def write_report(self):
        pass

# Human employee implements all
class Employee(Workable, Feedable, Sleepable, Manageable):
    def work(self):
        print("Working on tasks")
    
    def eat(self):
        print("Having lunch")
    
    def sleep(self):
        print("Sleeping at night")
    
    def attend_meeting(self):
        print("In meeting")
    
    def write_report(self):
        print("Writing report")

# Robot only implements what it can do
class Robot(Workable):
    def work(self):
        print("Processing tasks 24/7")
```

### ✅ E-commerce Example

```python theme={null}
# Instead of one huge OrderService interface
class OrderReader(ABC):
    @abstractmethod
    def find_by_id(self, order_id: UUID) -> Order:
        pass
    
    @abstractmethod
    def find_by_customer(self, customer_id: UUID) -> List[Order]:
        pass

class OrderWriter(ABC):
    @abstractmethod
    def save(self, order: Order) -> None:
        pass
    
    @abstractmethod
    def update(self, order: Order) -> None:
        pass

class OrderCanceller(ABC):
    @abstractmethod
    def cancel(self, order_id: UUID, reason: str) -> None:
        pass

# Services can depend on only what they need
class OrderDisplayService:
    def __init__(self, reader: OrderReader):  # Only needs read
        self.reader = reader

class CheckoutService:
    def __init__(self, writer: OrderWriter):  # Only needs write
        self.writer = writer
```

***

## 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

```python theme={null}
class OrderService:
    def __init__(self):
        self.db = PostgreSQLDatabase()  # Directly depends on implementation
        self.payment = StripePayment()
        self.email = SendGridEmail()
```

### ✅ Dependency Injection

```python theme={null}
# Abstractions
class Database(ABC):
    @abstractmethod
    def save(self, entity: Any) -> None:
        pass

class PaymentGateway(ABC):
    @abstractmethod
    def charge(self, amount: Decimal) -> PaymentResult:
        pass

class EmailService(ABC):
    @abstractmethod
    def send(self, to: str, subject: str, body: str) -> None:
        pass

# Implementations
class PostgreSQLDatabase(Database):
    def save(self, entity: Any) -> None:
        print("Saving to PostgreSQL")

class StripePaymentGateway(PaymentGateway):
    def charge(self, amount: Decimal) -> PaymentResult:
        print("Charging via Stripe")
        return PaymentResult(success=True)

# Service depends on abstractions
class OrderService:
    def __init__(
        self,
        database: Database,
        payment: PaymentGateway,
        email: EmailService
    ):
        self.database = database
        self.payment = payment
        self.email = email
    
    def place_order(self, order: Order) -> None:
        self.payment.charge(order.total)
        self.database.save(order)
        self.email.send(
            order.customer.email,
            "Order Confirmed",
            f"Your order {order.id} is confirmed"
        )

# Easy to swap implementations
service = OrderService(
    database=PostgreSQLDatabase(),
    payment=StripePaymentGateway(),
    email=SendGridEmailService()
)

# Easy to test with mocks
test_service = OrderService(
    database=MockDatabase(),
    payment=MockPayment(),
    email=MockEmail()
)
```

## SOLID Summary

| Principle                 | One-liner                    | Benefit               |
| ------------------------- | ---------------------------- | --------------------- |
| **S**ingle Responsibility | One reason to change         | Easier maintenance    |
| **O**pen/Closed           | Extend without modifying     | Safer changes         |
| **L**iskov Substitution   | Subtypes are interchangeable | Reliable polymorphism |
| **I**nterface Segregation | Small, focused interfaces    | Less coupling         |
| **D**ependency Inversion  | Depend on abstractions       | Flexible, testable    |

***

## 🎯 Quick Decision Guide

When designing classes, ask yourself:

<Steps>
  <Step title="Single Responsibility Check">
    **Question**: Does this class have only one reason to change?

    **Red Flag**: Class name contains "Manager", "Handler", "Utils", "Helper"

    **Fix**: Extract each responsibility into its own class
  </Step>

  <Step title="Open/Closed Check">
    **Question**: Can I add new behavior without modifying existing code?

    **Red Flag**: Long if-elif chains based on type

    **Fix**: Use polymorphism, Strategy pattern, or plugin architecture
  </Step>

  <Step title="Liskov Substitution Check">
    **Question**: Can I use any subtype wherever the parent is expected?

    **Red Flag**: Type checking before method calls, "not implemented" exceptions

    **Fix**: Redesign hierarchy or use composition instead
  </Step>

  <Step title="Interface Segregation Check">
    **Question**: Does every implementer need ALL methods in the interface?

    **Red Flag**: Empty stub implementations, methods throwing "not supported"

    **Fix**: Split into smaller, focused interfaces
  </Step>

  <Step title="Dependency Inversion Check">
    **Question**: Does this class depend on abstractions, not concretions?

    **Red Flag**: Using `new` or direct instantiation of dependencies

    **Fix**: Inject dependencies through constructor/parameters
  </Step>
</Steps>

***

## 💡 Interview Tips

<AccordionGroup>
  <Accordion title="How to Demonstrate SOLID" icon="star">
    During LLD interviews, explicitly mention when you're applying 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"*
  </Accordion>

  <Accordion title="When SOLID is Overkill" icon="triangle-exclamation">
    SOLID adds complexity. Don't apply it to:

    * Simple scripts or one-off utilities
    * Classes with only 1-2 methods
    * Early prototypes (refactor later)

    Apply SOLID when you expect:

    * Multiple developers
    * Long-term maintenance
    * Frequent changes/extensions
  </Accordion>

  <Accordion title="Testing Benefits" icon="flask">
    SOLID code is easy to test:

    ```python theme={null}
    # With DIP, testing is trivial:
    def test_order_service():
        mock_db = MockDatabase()
        mock_email = MockEmailService()
        service = OrderService(mock_db, mock_email)
        
        service.place_order(test_order)
        
        assert mock_db.save_called
        assert mock_email.send_called
    ```
  </Accordion>
</AccordionGroup>

<Tip>
  **Remember**: SOLID principles are guidelines, not rules. Apply them where they add value. Over-engineering is also a problem! The goal is clean, maintainable code - not perfect adherence to principles.
</Tip>

***

## Interview Questions

<AccordionGroup>
  <Accordion title="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?">
    **Strong Answer:**

    * 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()`, and `convert_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.

    **Red flag answer:** "Every method should be its own class" or "SRP means a class should only have one method." Both indicate a mechanical reading of SRP without understanding the underlying principle of cohesion.

    **Follow-ups:**

    1. You have a UserService with `create_user()`, `update_profile()`, `change_password()`, and `delete_user()`. A colleague says this violates SRP because it has four methods. How do you respond?
    2. 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?
  </Accordion>

  <Accordion title="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?">
    **Strong Answer:**

    * 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 `DiscountStrategy` interface with implementations like `StackAllDiscounts`, `BestDiscountOnly`, and `PriorityStackDiscounts`.
    * For "VIP always applies on top": you need rule priority and layering. I would add a `priority()` method to DiscountRule and a `is_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.

    **Red flag answer:** "OCP means you never modify existing code" (absolutist and impractical) or "Just add if-statements in PricingService to handle the stacking rules" (defeats the entire OCP architecture).

    **Follow-ups:**

    1. 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?
    2. The business now says "show the customer which discount was applied and why." How does this affect your DiscountRule interface?
  </Accordion>

  <Accordion title="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?">
    **Strong Answer:**

    * 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 if `r` is actually a Square, `setHeight(10)` also sets width to 10, so `r.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.

    **Red flag answer:** "Square IS a Rectangle, so it should extend Rectangle" (confuses mathematical taxonomy with software contracts) or "Just don't write code that assumes width and height are independent" (shifts the burden to every consumer).

    **Follow-ups:**

    1. 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?
    2. Java's `Collections.unmodifiableList()` returns a List that throws `UnsupportedOperationException` on `add()`. Is this an LSP violation?
  </Accordion>

  <Accordion title="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?">
    **Strong Answer:**

    * This is the tension between ISP and practical polymorphism. You need a shared type for the scheduler, but you do not want a fat interface that forces robots to implement `eat()` and `sleep()`.
    * The solution is a minimal shared interface defined by the consumer. Create a `Schedulable` interface with `get_availability()` and `assign_task()`. Both Employee and Robot implement Schedulable. The scheduler works with `List[Schedulable]` and does not know or care whether the entity eats or sleeps.
    * This means an Employee implements `Workable`, `Feedable`, `Sleepable`, `Manageable`, AND `Schedulable`. A Robot implements `Workable` and `Schedulable`. Each subsystem sees exactly the interface it needs. The cafeteria system sees only Feedable entities. The scheduler sees only Schedulable entities.
    * This pattern is called "Role Interfaces" -- each interface represents a role the object plays in a specific context, rather than a complete description of the object. It is ISP applied correctly: the consumer defines what it needs, not the producer.

    **Red flag answer:** "Just make Robot implement the full Worker interface with stub methods" (defeats ISP entirely) or "Use `isinstance` checks in the scheduler" (fragile, violates OCP because adding a new entity type requires modifying the scheduler).

    **Follow-ups:**

    1. With 5 narrow interfaces, how do you handle the complexity of a class implementing all 5? Does this lead to "header interface hell" where every class has a long list of implements?
    2. In Go, interfaces are satisfied implicitly (structural typing). How does that change the ISP trade-off compared to Python or Java where you explicitly declare implementation?
  </Accordion>

  <Accordion title="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.">
    **Strong Answer:**

    * 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.py` or `app_factory()`) that creates all dependencies and wires them. This is explicit and debuggable but verbose. (2) A DI container like `dependency-injector` that automates wiring based on type hints. You register bindings (`Database -> PostgreSQLDatabase`) and the container resolves the graph. (3) Framework-level DI like FastAPI's `Depends()`.
    * 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.

    **Red flag answer:** "Just use global variables" or "Import the concrete class directly wherever you need it." Both destroy testability and couple everything to specific implementations.

    **Follow-ups:**

    1. What is the difference between Dependency Injection and Dependency Inversion? They sound similar but are separate concepts.
    2. You need PostgreSQL in production, SQLite in development, and MockDB in tests. How do you configure this without if/elif chains?
  </Accordion>

  <Accordion title="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?">
    **Strong Answer:**

    * 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.

    **Red flag answer:** "SOLID should always be applied from the start" (ignores cost and context) or "YAGNI means never refactor" (ignores compounding technical debt).

    **Follow-ups:**

    1. 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?
    2. Can you give a real example where premature SOLID application caused more harm than the problem it was trying to prevent?
  </Accordion>

  <Accordion title="Look at all 5 SOLID principles together. Which two have the strongest synergy, and which two are most likely to conflict in practice?">
    **Strong Answer:**

    * 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.

    **Red flag answer:** "All 5 principles always work together perfectly" (suggests theoretical knowledge without practical experience) or "I just apply them all equally" (no judgment about context).

    **Follow-ups:**

    1. LSP and OCP both deal with subtyping and extension. Can you give an example where satisfying one forces you to violate the other?
    2. If you could only teach a junior developer TWO of the five SOLID principles, which two would give them the most leverage?
  </Accordion>

  <Accordion title="The DIP test example creates MockDatabase, MockPayment, and MockEmail. A QA engineer argues that mocking everything makes the tests meaningless. Who is right?">
    **Strong Answer:**

    * Both sides have merit. Unit tests with mocks verify that OrderService's logic is correct in isolation: does it call `charge()` before `save()`? 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=True` and 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.

    **Red flag answer:** "Always mock everything for speed" or "Never mock, only integration tests." Both are absolutist positions that ignore the cost-benefit trade-off at each testing layer.

    **Follow-ups:**

    1. 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?
    2. What are contract tests (like Pact), and how do they solve the "mocks drift from reality" problem?
  </Accordion>
</AccordionGroup>

***

## Interview Deep-Dive

<AccordionGroup>
  <Accordion title="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?">
    **Strong Answer:**

    * 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.

    **Follow-up: What is the risk of over-applying SRP? When have you seen it go wrong?**

    The risk is "class explosion" -- breaking a coherent responsibility into too many tiny classes. I once saw a codebase where someone split a UserService into UserCreator, UserUpdater, UserDeleter, UserFinder, UserValidator, UserSerializer -- six classes for basic CRUD. The cognitive overhead of navigating six files for one entity was worse than the original class. The test: if all the methods change for the same reason (user data schema changes), they belong in one class. SRP says "one reason to change," not "one method per class."
  </Accordion>

  <Accordion title="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?">
    **Strong Answer:**

    * 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.

    **Follow-up: How would you test this priority and exclusivity logic?**

    I would write unit tests for the PricingService with mock DiscountRules. Test cases: two rules in the same group where rule A gives 10% off and rule B gives 20% off -- verify only 20% is applied. Two rules in different groups -- verify both apply. A rule with is\_applicable returning False -- verify it is skipped. Rules with identical priority -- verify deterministic ordering (tie-break by insertion order or rule name). Since the PricingService depends on the DiscountRule abstraction (DIP), I can inject simple test implementations without touching any real pricing logic.
  </Accordion>

  <Accordion title="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?'">
    **Strong Answer:**

    * 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.

    **Follow-up: How do you convince your team to invest time in this migration?**

    I quantify the cost of the current violation. Every time a developer adds a new subclass, they have to implement methods they cannot support (raising NotImplementedError), write defensive isinstance checks in consumer code, and maintain documentation about which subclasses support which methods. I track how many bugs or incidents were caused by someone passing a Penguin to code that expected flying behavior. If I can show two production incidents in the last quarter caused by LSP violations, the migration pays for itself.
  </Accordion>

  <Accordion title="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?'">
    **Strong Answer:**

    * 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.

    **Follow-up: How does this relate to DI containers and frameworks?**

    DI containers (like Spring in Java, or dependency-injector in Python) automate the wiring. Instead of manually constructing OrderService(PostgreSQLDatabase(), StripePayment(), SendGridEmail()), the container reads configuration, resolves the dependency graph, and creates everything for you. The benefit is that adding a new dependency to OrderService does not require updating every call site. The trade-off is implicit magic -- a developer reading the code cannot immediately see where the dependencies come from without understanding the container's configuration. I prefer explicit constructor injection in smaller systems and containers in larger systems with dozens of services.
  </Accordion>
</AccordionGroup>
