Search Tech Journey

Find topics, journeys and posts

back to blog
engineeringintermediate 12m2026-06-09

SOLID Part 2 — ISP, DIP, and Design Patterns (Strategy, Factory, Observer)

Session 11 of the 48-session learning series.

Why this session matters

This is Session 11 of 48 in the OOP & Languages track. It builds on the rhythm of one focused topic, paced so you have time to actually absorb it rather than rush.

Agenda

  • ISP — don’t force consumers to depend on interfaces they don’t use
  • DIP — depend on abstractions, not concretions; the inversion shape
  • Strategy / Factory / Observer — the three patterns you’ll actually use
  • Patterns in Python — functions and Protocols beat heavy class hierarchies
  • When patterns help vs hurt — a short anti-pattern guide

Pre-read (skim before the session)

Deep dive

1. ISP — Interface Segregation Principle

No client should be forced to depend on methods it does not use.

The smell: a 'fat' interface where most implementers only care about half the methods, raising NotImplementedError for the rest.

Bad — one giant PrinterDevice interface:

class PrinterDevice(Protocol):
    def print(self, doc): ...
    def scan(self, doc): ...
    def fax(self, doc): ...
    def staple(self, doc): ...

A basic printer doesn't fax or staple. It either fakes the methods or breaks LSP.

Good — small, role-based interfaces:

class Printer(Protocol):
    def print(self, doc): ...

class Scanner(Protocol):
    def scan(self, doc): ...

class Fax(Protocol):
    def fax(self, doc): ...

class Stapler(Protocol):
    def staple(self, doc): ...

class MultiFunctionDevice(Printer, Scanner, Fax): ...    # opts in by composition

Consumers depend only on what they need: def archive(scanner: Scanner) doesn't drag in print/fax.

2. DIP — Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions.

The shape: when a high-level policy module needs to talk to a low-level infrastructure module, invert the relationship by introducing an abstraction the high-level module owns and the low-level module implements.

Before:

# high-level
class OrderService:
    def __init__(self):
        self.db = PostgresOrderRepo()   # direct dep on postgres

After:

class OrderRepo(Protocol):
    def save(self, order: Order) -> None: ...
    def get(self, id: str) -> Order: ...

class OrderService:
    def __init__(self, repo: OrderRepo):
        self.repo = repo

# in adapter layer (lives near infra)
class PostgresOrderRepo:
    def save(self, order): ...
    def get(self, id): ...

Now OrderService knows about OrderRepo (its own abstraction) and Postgres knows about OrderRepo (implements it). The arrow of dependency inverted — high-level no longer imports the low-level.

This is the backbone of clean / hexagonal / onion / ports-and-adapters architecture. The pattern is everywhere because it makes substitution (and testing) trivial — swap in an InMemoryOrderRepo in tests, no patches.

3. Strategy pattern (you saw it in S04)

Encapsulate an algorithm in an object; swap implementations at runtime.

class Sort(Protocol):
    def sort(self, items: list) -> list: ...

class Quicksort:
    def sort(self, items): ...

class Mergesort:
    def sort(self, items): ...

class Pipeline:
    def __init__(self, sorter: Sort):
        self.sorter = sorter
    def run(self, items):
        return self.sorter.sort(items)

In Python, Strategy is often just a callable:

class Pipeline:
    def __init__(self, sort_fn):
        self.sort_fn = sort_fn   # any callable list→list
    def run(self, items):
        return self.sort_fn(items)

pipeline = Pipeline(sort_fn=sorted)

Don’t build a class hierarchy when a function works.

4. Factory pattern

Encapsulate construction so consumers ask 'give me an X for this input' instead of if/elif/elif over types.

def payment_for(method: str) -> PaymentStrategy:
    return {
        "card":   CardPayment(),
        "upi":    UPIPayment(),
        "paypal": PayPalPayment(),
    }[method]

In Python it’s usually just a function returning the right object. The full GoF AbstractFactory is overkill 90% of the time.

Use a factory when:

  • Construction is non-trivial (config, secrets, connection pools).
  • You’re hiding which concrete type from the caller.
  • You want a single registration point (register("card", CardPayment)) for plugins.

5. Observer pattern (a.k.a. pub/sub)

Subjects publish events; observers subscribe; subject doesn’t know who’s listening.

from collections import defaultdict

class EventBus:
    def __init__(self):
        self._subs: dict[type, list] = defaultdict(list)
    def subscribe(self, event_type, handler):
        self._subs[event_type].append(handler)
    def publish(self, event):
        for h in self._subs[type(event)]:
            h(event)

bus = EventBus()
bus.subscribe(OrderPlaced, send_confirmation_email)
bus.subscribe(OrderPlaced, update_inventory)
bus.publish(OrderPlaced(order_id="..."))

In-process Observer is a stepping-stone to real pub/sub (Kafka, NATS, Pub/Sub) across services. The shape is the same; the bus is just remote.

6. Patterns in Python — don't over-Java-ify

A lot of GoF patterns dissolve in Python because:

  • First-class functions → Strategy / Command often become functions.
  • Decorators → the Decorator pattern is literally syntax.
  • Protocol → no need for abstract base classes for most interfaces.
  • Module-level state → Singleton is instance = ClassName() at module load.

Reach for a class hierarchy only when:

  • Multiple methods share state (__init__ collaborators).
  • The pattern truly has multiple methods (Strategy with sort + validate + report).
  • Polymorphism is genuinely needed across many call sites.

7. Anti-patterns to call out in code review

  • God classes — OrderService with 27 methods. Split by SRP.
  • Anaemic models — dataclasses with no behaviour + a parallel OrderManager. Either grow the model or accept it's a DTO.
  • Premature factories — a factory for one concrete type.
  • Singletons hidden behind globals — untestable. Make them injectable.
  • isinstance ladders — you wanted polymorphism; refactor with strategies or pattern matching.
  • Layer cake with no behaviour — 5 layers, each forwards to the next. Cut layers.

8. A small worked refactor

Before — a notification function with 4 if-branches and infrastructure baked in:

def notify(user, channel, msg):
    if channel == "email":
        smtp = smtplib.SMTP("mail.local"); smtp.sendmail(...)
    elif channel == "sms":
        twilio.send(user.phone, msg)
    elif channel == "push":
        fcm.send(user.device_token, msg)
    elif channel == "slack":
        slack.post_message(user.slack_id, msg)

After — strategy + DIP:

class Notifier(Protocol):
    def notify(self, user, msg): ...

class EmailNotifier:    ...
class SmsNotifier:      ...
class PushNotifier:     ...
class SlackNotifier:    ...

def get_notifier(channel: str) -> Notifier:
    return REGISTRY[channel]      # factory

# usage in service
class NotificationService:
    def __init__(self, notifier: Notifier):
        self.notifier = notifier
    def notify(self, user, msg):
        self.notifier.notify(user, msg)

Wiring (e.g. in your app composition root) is the only place that knows about all four implementations.

9. When patterns hurt

  • You added a Strategy + Factory + Observer for a CLI tool that runs once a week.
  • You wrapped every dataclass in an interface 'in case we add a second one'.
  • You have 5 layers of OrderHandlerFactoryProvider because the framework demanded it.

Reach for the simplest thing that still passes the real test: can two engineers safely change two parts of the system at the same time?

10. What's next (Session 16 — Concurrency)

We shift from class-shape questions to runtime-shape questions: threads, asyncio, the GIL, actor models, and where each one is the right answer.

Reading material

Books:

  • Design Patterns: Elements of Reusable Object-Oriented Software — Gamma, Helm, Johnson, Vlissides ("Gang of Four")
  • Head First Design Patterns, 2nd ed. — Freeman, Robson (easier on-ramp than GoF; Strategy / Factory / Observer covered)
  • Clean Architecture — Robert C. Martin (the DIP chapter is gold; "The Dependency Rule")

Papers / essays:

Official docs:

Blog posts:

In-depth research material

Videos

LeetCode — Design Twitter

  • Link: https://leetcode.com/problems/design-twitter/
  • Difficulty: Medium
  • Why this problem: User→tweets list + followee set; getNewsFeed merges via min-heap of top-k by timestamp.
  • Time-box: 30 minutes. Look up the editorial only after.

Post-session checklist

By the end of this session you should be able to:

  • Spot an ISP violation (a fat interface) and split it into role-based interfaces.
  • Apply DIP to invert a direct DB dependency.
  • Implement Strategy, Factory, Observer in idiomatic Python (functions where possible).
  • Call out 3 anti-patterns and propose refactors.
  • Solve design-twitter using composition + min-heap merge.

Generated from sessions_data.py + content_part*.py. To edit a video / leetcode / title, edit the data file and re-run write_sessions.py.