Design Patterns in Practice: Factory, Observer, and Strategy

If you want to level up your code’s maintainability and flexibility, you need more than just clean syntax. Design patterns give your team a shared language and proven blueprints for solving common architectural problems. In this post, you’ll see how the Factory, Observer, and Strategy patterns work in production code—and how they make reviews, refactoring, and onboarding smoother for everyone.

Key Takeaways:

  • Understand the core purpose and mechanics of the Factory, Observer, and Strategy patterns
  • See complete, runnable code examples for each pattern using realistic business logic
  • Compare the patterns side-by-side with actionable guidance on when to use which
  • Spot common mistakes and learn best practices from production experience

Why Design Patterns Matter in Real Projects

Design patterns are not just academic jargon—they solve recurring architectural problems with proven, reusable solutions. In code reviews, mentioning “Observer” or “Factory” collapses a page of intent into a single word. Patterns keep teams aligned and codebases coherent, especially in remote or rapidly scaling teams (Software Engineering: A Modern Approach, Ch. 6).

Here’s what design patterns do for you:

  • Provide a shared vocabulary that speeds up communication
  • Encourage loose coupling and extensibility
  • Reduce bugs by reusing well-tested architectures
  • Help new team members ramp up—patterns are documented everywhere

Patterns like Factory, Observer, and Strategy are “workhorses” that show up in real-world backend, frontend, and cloud code. You’ll see them in Python, Java, and JavaScript ecosystems (source).

Factory Pattern: Decoupling Object Creation

What Problem Does Factory Solve?

When your code needs to create objects, but you want to avoid hard-wiring specific classes everywhere, use the Factory Pattern. It encapsulates object creation, so you can add new types without rewriting the business logic.

Factory Pattern in Python: Example

# Python 3.10+
from abc import ABC, abstractmethod

class Notification(ABC):
    @abstractmethod
    def send(self, message: str) -> None:
        pass

class EmailNotification(Notification):
    def send(self, message: str) -> None:
        print(f"Email sent: {message}")

class SMSNotification(Notification):
    def send(self, message: str) -> None:
        print(f"SMS sent: {message}")

class NotificationFactory:
    @staticmethod
    def create(channel: str) -> Notification:
        if channel == "email":
            return EmailNotification()
        elif channel == "sms":
            return SMSNotification()
        else:
            raise ValueError(f"Unknown channel: {channel}")

# Usage
notif = NotificationFactory.create("email")
notif.send("Your order has shipped.")  # Output: Email sent: Your order has shipped.

What’s happening? The factory method NotificationFactory.create() chooses which subclass to instantiate based on a string. If you add a PushNotification class, no business logic changes—only the factory needs updating.

This pattern is critical when your objects have complex setup logic (e.g., reading secrets, injecting dependencies), or when you need to support plugin architectures.

Observer Pattern: Decoupling Event Handling

What Problem Does Observer Solve?

When one object changes and others need to react, but you don’t want tight coupling, use the Observer Pattern. It lets you attach any number of listeners to a subject, and notifies them automatically when something happens (source).

Observer Pattern in Python: Example

# Python 3.10+
from typing import List, Protocol

class Observer(Protocol):
    def update(self, data: dict) -> None:
        ...

class OrderSubject:
    def __init__(self) -> None:
        self._observers: List[Observer] = []

    def attach(self, observer: Observer) -> None:
        self._observers.append(observer)

    def notify(self, data: dict) -> None:
        for observer in self._observers:
            observer.update(data)

class EmailNotifier:
    def update(self, data: dict) -> None:
        print(f"Email: Order {data['order_id']} status changed to {data['status']}.")

class InventoryUpdater:
    def update(self, data: dict) -> None:
        print(f"Inventory: Update stock for item {data['item_id']}.")

# Usage
subject = OrderSubject()
subject.attach(EmailNotifier())
subject.attach(InventoryUpdater())

subject.notify({'order_id': 123, 'status': 'shipped', 'item_id': 'SKU-987'})
# Output:
# Email: Order 123 status changed to shipped.
# Inventory: Update stock for item SKU-987.

What’s happening? OrderSubject manages a list of observers, each with an update method. When notify is called, all observers react. You can add or remove observers at runtime, supporting plugins and decoupled integrations.

Observer is the pattern behind event buses, signal/slot systems, and UI frameworks’ data binding.

Strategy Pattern: Decoupling Algorithms

What Problem Does Strategy Solve?

When you need to swap out algorithms or business rules at runtime—without changing the code that uses them—the Strategy Pattern is your tool. It lets you switch logic (sorting, fee calculation, etc.) via configuration or user input.

Strategy Pattern in Python: Example

# Python 3.10+
from abc import ABC, abstractmethod

class ShippingStrategy(ABC):
    @abstractmethod
    def calculate(self, order: dict) -> float:
        pass

class StandardShipping(ShippingStrategy):
    def calculate(self, order: dict) -> float:
        return 5.0  # Flat rate

class ExpressShipping(ShippingStrategy):
    def calculate(self, order: dict) -> float:
        return 15.0 + 0.1 * order['weight']  # Higher base, per-weight charge

class ShippingCostCalculator:
    def __init__(self, strategy: ShippingStrategy) -> None:
        self.strategy = strategy

    def calculate(self, order: dict) -> float:
        return self.strategy.calculate(order)

# Usage
order = {'weight': 42}
calculator = ShippingCostCalculator(StandardShipping())
print(calculator.calculate(order))  # Output: 5.0

calculator.strategy = ExpressShipping()
print(calculator.calculate(order))  # Output: 19.2

What’s happening? You can swap ShippingStrategy objects at runtime. This is powerful for A/B testing, feature flags, or user-selectable options. The client code never needs to know which strategy is in use.

Patterns like choosing between different sorting algorithms are classic uses of Strategy.

Comparison Table: When to Use Each Pattern

PatternObjectiveWhereTypical useWhat it does
FactoryObject creationClient from specific classesNotification services, database drivers, plugins
  • Centralizes creation logic
  • Easy to add types
  • Can become monolithic if overused
ObserverEvent notificationPublisher from subscribersEvent buses, GUI frameworks, data sync
  • Flexible, runtime extensible
  • Low coupling
  • Difficult to trace execution flow
StrategyAlgorithm selectionClient from algorithm detailsPayment processing, sorting, authentication
  • Interchangeable logic
  • Good for testing
  • Too many small classes if misapplied

Pitfalls and Pro Tips in Pattern Implementation

  • Factory Pattern: Don’t let your factory turn into a “God class” that knows about every possible type. Use registration (maps of class names to constructors) when scaling up.
  • Observer Pattern: Be careful of memory leaks—observers must be detached when no longer needed, especially in long-lived objects or GUIs.
  • Strategy Pattern: Don’t over-engineer—if you only have two algorithms and they never change, a simple if or match may suffice.
  • Always provide documentation in code: Write which pattern is used and why. This helps code reviews stay focused on intent, not style (reference).
  • Favor composition over inheritance. All three patterns are about wiring objects together, not building deep class hierarchies.

For more on how patterns underpin large-scale workflows, see this guide to Git workflow strategies.

Conclusion and Next Steps

Factory, Observer, and Strategy patterns are foundational tools—not just theory, but blueprints you’ll see in production code across every major language. Start by identifying places where object creation, event handling, or algorithm selection are creating tight coupling or code duplication. Refactor those spots using the patterns above, and your codebase will be easier to extend, test, and onboard to.

Want to deepen your understanding? Compare these patterns with others like Singleton, Decorator, or Adapter in your next project, and see where each shines. For further reading, check out the Software Engineering: A Modern Approach and real-world pattern guides from JS Schools.