📘 Design Patterns for Builders — Part 2

In Part 1, we used Strategy to remove if-else from business logic. Now we fix the next problem: who decides which strategy to create—and how?


⚡ TL;DR

  • Don’t scatter SomeClass() across your codebase
  • Centralize creation behind a factory
  • Separate usage from creation
  • Evolve toward registry/DI as systems grow

The Problem (Picking Up From Strategy)

After introducing Strategy, you often end up here:

def get_strategy(method: str) -> PaymentStrategy:
    if method == "credit_card":
        return CreditCardPayment()
    elif method == "paypal":
        return PayPalPayment()
    elif method == "upi":
        return UPIPayment()
    else:
        raise ValueError("Unsupported payment method")

We removed conditionals from business logic… but moved them into selection/creation.


Why This Breaks in Real Systems

This isn’t just about an if-else living elsewhere.

  • Every new type → modify this function (same Open/Closed violation)
  • Creation logic spreads across modules over time
  • Dependencies (clients, configs) leak into callers
  • Hard to test (you need to wire real deps or patch everywhere)

You’re coupling object creation with call sites.

At scale, this becomes: - a merge-conflict hotspot

  • inconsistent construction across services
  • subtle bugs due to misconfigured instances

What We Actually Want

We don’t want:

“Every part of the system deciding how to create objects”

We want:

“A single place responsible for constructing the right object”

Even better:

Decouple creation from usage, and make construction replaceable

That’s the Factory Pattern.


The Idea (Without the Textbook)

  • Encapsulate object creation
  • Hide construction details
  • Return a ready-to-use instance via a stable API

At a deeper level:

We separate how an object is built from how it’s used.


Refactoring with a Basic Factory

Step 1: Introduce a factory

class PaymentFactory:
    @staticmethod
    def get_strategy(method: str) -> PaymentStrategy:
        if method == "credit_card":
            return CreditCardPayment()
        elif method == "paypal":
            return PayPalPayment()
        elif method == "upi":
            return UPIPayment()
        else:
            raise ValueError("Unsupported payment method")

Step 2: Use it from business logic

def process_payment(method: str, amount: int):
    strategy = PaymentFactory.get_strategy(method)
    return strategy.pay(amount)

Now callers don’t know (or care) how objects are created.


What Just Improved?

  • ✅ Creation logic is centralized\
  • ✅ Call sites are cleaner and consistent\
  • ✅ Easier to change construction (one place)\
  • ✅ Better test seams (mock the factory or registry)

We reduced construction coupling across the codebase.


⚠️ But This Is Still Not Enough

A big if-else inside a factory is still a smell.

  • It will keep growing
  • It still violates Open/Closed
  • It doesn’t scale across teams/modules

We need a data-driven approach.


A Better Approach: Registry-Based Factory

Replace conditionals with a registry (map):

class PaymentFactory:
    _registry = {}

    @classmethod
    def register(cls, key: str, ctor):
        cls._registry[key] = ctor

    @classmethod
    def get_strategy(cls, key: str) -> PaymentStrategy:
        try:
            return cls._registry[key]()
        except KeyError:
            raise ValueError("Unsupported payment method")

Register implementations:

PaymentFactory.register("credit_card", CreditCardPayment)
PaymentFactory.register("paypal", PayPalPayment)
PaymentFactory.register("upi", UPIPayment)

Real-World Note: Factories Handle Dependencies

class StripePayment(PaymentStrategy):
    def __init__(self, client, api_key: str):
        self.client = client
        self.api_key = api_key

    def pay(self, amount):
        return self.client.charge(amount, self.api_key)

Factory encapsulates wiring:

class PaymentFactory:
    @staticmethod
    def stripe():
        return StripePayment(client=StripeClient(), api_key="key")

Toward Dependency Injection

Factories often evolve into DI:

  • Config-driven construction
  • Lifecycle management
  • Easy test overrides

FastAPI Integration

from fastapi import FastAPI

app = FastAPI()

@app.post("/pay")
def pay(method: str, amount: int):
    strategy = PaymentFactory.get_strategy(method)
    return strategy.pay(amount)

⚠️ Common Mistakes

  • Giant if-else factories
  • Over-engineering simple creation
  • Hiding too much logic

Mental Model

Factory Pattern = Centralize and abstract object creation so callers depend on what they use, not how it’s built.


Builder Notes

  • Strategy + Factory is a powerful combination
  • Prefer registry-based approach
  • Evolves naturally into DI

Final Thought

If you see:

SomeClass()

everywhere, ask:

“Should construction be centralized?”


Next in the series: Decorator Pattern