Skip to content

Factory Method (Creational)

What Problem Does It Solve?

Object creation logic is complex, conditional, or scattered across the codebase. Callers shouldn't need to know the concrete class they're instantiating or the details of how it's built.

Analogy: You walk into a sandwich shop and say "I'll have a BLT." You don't chop the lettuce, fry the bacon, or toast the bread. The kitchen (factory) handles that. You just name what you want and get a finished sandwich.

How to Identify When to Use It

  • You have if/elif/else chains that pick which class to instantiate
  • Callers frequently pass raw config values to constructors
  • You want to decouple client code from concrete implementation classes
  • The creation process involves setup steps that are easy to forget

Questions to ask yourself:

  • "If I add a new type, how many places do I need to modify?"
  • "Does the caller actually care about the concrete type or just the interface?"

Red flags:

  • if type == "pdf": branches scattered in unrelated functions
  • New developers frequently forget to initialize objects properly
  • Constructors with the same parameter lists repeated everywhere

How to Apply It

  1. Define a common interface (abstract base class) for the products
  2. Create concrete product classes that implement the interface
  3. Write a factory function (or class) that takes a discriminator and returns the correct concrete product
  4. (Optional) Register new products without modifying the factory (registry pattern)
python
from abc import ABC, abstractmethod


class Exporter(ABC):
    @abstractmethod
    def export(self, data: str) -> str:
        pass


class PDFExporter(Exporter):
    def export(self, data: str) -> str:
        return f"PDF: {data}"


class CSVExporter(Exporter):
    def export(self, data: str) -> str:
        return f"CSV: {data}"


class JSONExporter(Exporter):
    def export(self, data: str) -> str:
        return f"JSON: {data}"


def exporter_factory(format: str) -> Exporter:
    exporters = {
        "pdf": PDFExporter,
        "csv": CSVExporter,
        "json": JSONExporter,
    }
    cls = exporters.get(format)
    if cls is None:
        raise ValueError(f"Unknown format: {format}")
    return cls()


# Client code — no knowledge of concrete classes
exporter = exporter_factory("csv")
print(exporter.export("hello,world"))

Real-World Example

python
from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass
class PaymentRequest:
    amount: float
    currency: str
    method: str


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


class StripeGateway(PaymentGateway):
    def charge(self, amount: float, currency: str) -> str:
        return f"Stripe: charged {currency} {amount:.2f}"


class PayPalGateway(PaymentGateway):
    def charge(self, amount: float, currency: str) -> str:
        return f"PayPal: charged {currency} {amount:.2f}"


class BraintreeGateway(PaymentGateway):
    def charge(self, amount: float, currency: str) -> str:
        return f"Braintree: charged {currency} {amount:.2f}"


def payment_factory(method: str) -> PaymentGateway:
    mapping = {
        "stripe": StripeGateway,
        "paypal": PayPalGateway,
        "braintree": BraintreeGateway,
    }
    gateway = mapping.get(method)
    if not gateway:
        raise ValueError(f"Unsupported payment method: {method}")
    return gateway()


# Adding a new gateway (e.g., SquareGateway) only requires:
# 1. Create the class
# 2. Add it to mapping — no client code changes


request = PaymentRequest(amount=49.99, currency="USD", method="stripe")
gateway = payment_factory(request.method)
result = gateway.charge(request.amount, request.currency)
print(result)

Common Mistakes / Pitfalls

  • Factory that doesn't simplify: If the factory just maps strings to classes with the same constructor signature, it adds indirection without value.
  • Too many factories: One factory per module is reasonable. Don't create a factory for every single class.
  • Forgetting to register new products: Use a registry/plugin system or automated discovery to avoid manual updates.
  • Returning partially initialized objects: The factory must fully construct the object before returning it.
  • Abstract Factory — creates families of related factories
  • Dependency injection — factories can be injected rather than called statically
  • Registry pattern — dynamic product registration

<<>> with ♥️ by S@Nchit