Skip to content
LLD Learn/SOLID Principles
Browsing as a guest. Sign in to save your progress and earn XP as you complete chapters.

SOLID in One Interview: A Full Before/After Refactor

9 min read

You'll learn to

  • -Walk a single messy class through all five SOLID principles until it becomes an extensible, testable design
  • -Practice narrating the refactor out loud the way a strong interview answer actually sounds

Each SOLID principle in isolation is straightforward. The actual interview skill is applying all five together, in the right order, to one messy class - and narrating your reasoning the whole way, since a silent refactor earns no credit. This chapter walks one realistic starting point (an OrderProcessor that has grown every responsibility over several sprints) through all five principles in sequence.

The Starting Point

Where most real codebases actually start
class OrderProcessor:
    def process(self, order, payment_type: str):
        # validation
        if not order.items:
            raise ValueError("Empty order")
        # payment - branches on type, grows with every new payment method
        if payment_type == "credit_card":
            charge_credit_card(order.total)
        elif payment_type == "paypal":
            charge_paypal(order.total)
        # persistence - talks directly to MySQL
        mysql_connection.execute("INSERT INTO orders ...")
        # notification
        send_email(order.customer_email, "Order confirmed")

Applying SRP: Separate the Four Reasons to Change

Validation rules, payment processing, persistence, and notification each change for a different reason and a different stakeholder. Split into OrderValidator, PaymentProcessor, OrderRepository, and OrderNotifier, with OrderProcessor becoming a thin orchestrator that calls each in sequence.

Applying OCP: Kill the Payment-Type Branch

The if/elif on payment_type is the OCP smell from two chapters ago. Replace it with a PaymentStrategy interface and one class per payment method (CreditCardPayment, PayPalPayment), so a new payment method is a new class, not a new elif in a method every existing payment type already depends on.

Checking LSP: Do All PaymentStrategy Implementations Actually Substitute Cleanly?

Before moving on, sanity-check the new hierarchy: does every PaymentStrategy implementation charge() without surprising a caller that only knows the interface? If, say, a GiftCardPayment needs a partial-charge concept the interface never promised, that is caught here, before it becomes a production bug - not after.

Applying ISP: Split OrderRepository If It Has Grown Unrelated Methods

If OrderRepository has accumulated methods only some callers need (say, generate_analytics_report() alongside save() and find_by_id()), split it - callers that only ever save and fetch orders should not depend on a reporting method they never call.

Applying DIP: Depend on Interfaces at the Top

The fully refactored orchestrator
class OrderProcessor:
    def __init__(self, validator: OrderValidator, payment: PaymentStrategy,
                 repo: OrderRepository, notifier: OrderNotifier):
        self._validator = validator
        self._payment = payment
        self._repo = repo
        self._notifier = notifier

    def process(self, order: Order) -> None:
        self._validator.validate(order)
        self._payment.charge(order.total)
        self._repo.save(order)
        self._notifier.notify(order.customer_email, "Order confirmed")

OrderProcessor now depends on four interfaces, injected through its constructor - none of it depends on a concrete database, a concrete payment provider, or a concrete email client. Every one of those four collaborators can be swapped, mocked in a test, or extended with a new implementation without ever touching OrderProcessor again.

When narrating this out loud in an interview, name the principle as you apply each step ("this is an SRP split because...", "this removes the OCP violation because..."). It signals you are reasoning from principles, not pattern-matching from memory.

Interview Signal

After this refactor, the interviewer asks: "How would you unit test OrderProcessor.process() without hitting a real database or a real payment API?"

Weak Answer

"I'd set up a test database and a sandbox payment account to run the test against."

Strong Answer

"I wouldn't need either - since OrderProcessor depends on the OrderRepository and PaymentStrategy interfaces, I'd inject a fake in-memory repository and a fake payment strategy that just records what it was called with. The test verifies OrderProcessor's orchestration logic in isolation, which is exactly the payoff of applying Dependency Inversion in the first place."

Check Yourself1 / 3

In the OrderProcessor refactor, what specifically triggers applying the Single Responsibility Principle?

ScaleDojo Logo
Initializing ScaleDojo