Design PatternsIntermediate 12 min Lesson 2 of 13

Factory — Deciding What to Build

Move the decision of which class to create into one place, so the rest of your code depends only on the interface.

Design Patterns · Lesson 2 of 13
0/13 done(0%)

What is it? #

A factory is code whose job is to decide which concrete class to create and return it.

Without one, the decision gets duplicated. Five places each contain the same if provider == "stripe": ... elif provider == "razorpay": ..., and adding a sixth provider means finding all five.

With a factory, that decision lives in one function or class. Everything else receives an object that satisfies an interface and never learns which one it got.

There are two common shapes: a simple factory function that maps a key to a class, and the Factory Method pattern where subclasses decide what to create.

Think of it like this #

A car rental desk. You ask for "a small automatic" and receive keys. Which exact model you get is the desk's decision, based on what is available.

You drive whatever arrives because every car has the same controls. If you had to know the exact model in advance, every branch would need different instructions.

Simple example #

An application supports three payment providers. Checkout, refunds and reconciliation all need a client, and none of them should contain the provider-choosing logic.

Code #

PYTHON
from abc import ABC, abstractmethod


class PaymentGateway(ABC):
    @abstractmethod
    def charge(self, amount: float, token: str) -> str: ...

    @abstractmethod
    def refund(self, transaction_id: str) -> bool: ...


class StripeGateway(PaymentGateway):
    def __init__(self, api_key): self.api_key = api_key
    def charge(self, amount, token): return f"stripe-{amount}"
    def refund(self, transaction_id): return True


class RazorpayGateway(PaymentGateway):
    def __init__(self, key_id, key_secret): self.key_id = key_id
    def charge(self, amount, token): return f"razorpay-{amount}"
    def refund(self, transaction_id): return True


class SandboxGateway(PaymentGateway):
    """Used in development and tests — no network."""
    def __init__(self): self.charges = []
    def charge(self, amount, token):
        self.charges.append(amount)
        return f"sandbox-{amount}"
    def refund(self, transaction_id): return True


# --- The factory: the ONLY place that knows about concrete gateways ---
def make_gateway(config: dict) -> PaymentGateway:
    provider = config.get("provider", "sandbox")
    if provider == "stripe":
        return StripeGateway(api_key=config["stripe_key"])
    if provider == "razorpay":
        return RazorpayGateway(key_id=config["rzp_id"], key_secret=config["rzp_secret"])
    if provider == "sandbox":
        return SandboxGateway()
    raise ValueError(f"unknown payment provider: {provider}")


# --- Registry variant: adding a provider requires no edit to the factory ---
GATEWAYS: dict[str, type[PaymentGateway]] = {}

def register(name):
    def wrap(cls):
        GATEWAYS[name] = cls
        return cls
    return wrap


@register("paypal")
class PayPalGateway(PaymentGateway):
    def charge(self, amount, token): return f"paypal-{amount}"
    def refund(self, transaction_id): return True


def make_registered(name: str) -> PaymentGateway:
    try:
        return GATEWAYS[name]()
    except KeyError:
        raise ValueError(f"unknown provider: {name}") from None


# Callers depend only on the interface
gateway = make_gateway({"provider": "sandbox"})
print(gateway.charge(999.0, "tok_abc"))     # sandbox-999.0
TEXT
When to use it
  - several interchangeable implementations chosen at runtime or by config
  - construction needs arguments the caller should not have to know
  - you want one place to change when a new implementation is added

When NOT to use it
  - only one implementation exists and none is planned
  - construction is a single obvious call
  - the factory becomes a dumping ground for unrelated creation logic

How it works #

PaymentGateway defines what every provider must do. Callers depend on this and nothing else.

make_gateway is the factory. It reads configuration, picks a class, supplies whatever credentials that class needs, and returns it. All provider-specific construction knowledge is contained here.

Notice that each gateway needs different constructor arguments — Stripe takes one key, Razorpay takes two. Hiding that variation is a large part of the factory's value: callers ask for a gateway, not for a correctly-configured Razorpay client.

Returning SandboxGateway by default means local development and tests work with no credentials at all.

The registry variant goes one step further. New providers register themselves with a decorator, so adding one does not edit the factory. That satisfies the Open/Closed Principle, at the cost of the mapping being less obvious to read.

An error for an unknown provider matters. Silently falling back to a default would mean a typo in configuration sends real payments to a sandbox — or worse, the reverse.

Real-world use #

Configuration-driven implementation choice is everywhere: database drivers, storage backends, cache layers, authentication providers, export formats, notification channels.

Frameworks use the registry variant heavily. Serializer registries, plugin systems and command registries all map a name to a class.

Factories also centralise expensive or fiddly setup. Building an HTTP client with the right timeouts, retries and headers belongs in one factory rather than repeated at twenty call sites.

For tests, a factory that returns a sandbox implementation by default is a quiet but significant productivity win: nobody needs credentials to run the suite.

Common mistakes #

  • Letting the factory return different interfaces depending on the input, so callers must check types.
  • Putting business logic inside the factory. It should create objects, not make decisions about them.
  • Growing one factory that creates twenty unrelated things.
  • Falling back to a default silently instead of raising on unknown configuration.
  • Adding a factory when there is exactly one implementation.

Practice #

Write a make_storage(config) factory returning local disk, in-memory or cloud storage based on a config key, each requiring different constructor arguments. Then add a fourth backend using the registry variant and confirm the factory function needed no changes.

Quick quiz

  1. 1. What problem does a factory solve?

  2. 2. Why is it useful that each implementation takes different constructor arguments?

  3. 3. What does the registry variant add?

  4. 4. Why raise on an unknown provider rather than defaulting?

  5. 5. When is a factory unnecessary?

Summary

  • A factory centralises the decision of which implementation to create.
  • Callers depend on the interface and never name a concrete class.
  • It hides construction differences between implementations.
  • A registry lets new implementations be added without editing the factory.
  • Skip it when there is only one implementation.