đ Design Patterns for Builders — Part 2
In Part 1, we used Strategy to remove
if-elsefrom 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-elsefactories - 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
