bigonotes
CS ConceptsDesign Patterns

Factory Method Pattern

Move object creation out of business logic with a Simple Factory, a registry, or the GoF Factory Method, and know when each fits.

7 min readLast updated Design Patterns

Every ConcreteClass() call in your business logic hard-codes a decision about which class to use. Factories move that decision somewhere else, so the code that uses an object only depends on its interface. This note covers the family: the Simple Factory idiom (one place that creates objects), a registry-based factory, and the GoF Factory Method pattern (subclasses decide what to create).

Intent#

Define an interface for creating an object, but let subclasses decide which class to instantiate. (GoF)

Analogy: a restaurant chain. Every franchise follows the same "take order → make food → serve" process, but what gets made depends on the franchise: the Italian one makes Neapolitan pizza, another makes a local variation.

The problem: creation tangled with business logic#

Here the delivery planner also decides which transport class to build:

class LogisticsApp:
    def plan_delivery(self, kind: str) -> None:
        if kind == "truck":
            transport = Truck()
        elif kind == "ship":
            transport = Ship()
        elif kind == "plane":
            transport = Plane()
        else:
            raise ValueError(f"Unknown transport: {kind}")
        # Every new transport type means editing this method

        transport.deliver()

What goes wrong:

  1. Tight coupling. LogisticsApp knows every concrete transport class.
  2. Violates Open/Closed. Adding Train means editing working code.
  3. Violates Single Responsibility. The method plans deliveries and decides how objects are constructed.
  4. Duplication. The same if/else block tends to get copy-pasted (a RefundService needs it too).
  5. Hard to test. You can't easily substitute a fake transport.

Option 1: Simple Factory (an idiom, not a GoF pattern)#

Idea: "I don't want constructor calls for concrete classes scattered around. Put creation in one place." Like a restaurant with one kitchen: you say what you want, the kitchen makes it.

from abc import ABC, abstractmethod


class Pizza(ABC):
    @abstractmethod
    def prepare(self) -> None: ...

    @abstractmethod
    def bake(self) -> None: ...

    @property
    @abstractmethod
    def name(self) -> str: ...


class CheesePizza(Pizza):
    def prepare(self) -> None:
        print("Adding cheese topping")

    def bake(self) -> None:
        print("Baking 15 minutes at 350°F")

    @property
    def name(self) -> str:
        return "Cheese Pizza"


class VeggiePizza(Pizza):
    def prepare(self) -> None:
        print("Adding mushrooms, peppers, onions")

    def bake(self) -> None:
        print("Baking 20 minutes at 300°F")

    @property
    def name(self) -> str:
        return "Veggie Pizza"


class SimplePizzaFactory:
    """The Simple Factory: all creation decisions live here."""

    @staticmethod
    def create_pizza(kind: str) -> Pizza:
        match kind.lower():
            case "cheese":
                return CheesePizza()
            case "veggie":
                return VeggiePizza()
            case _:
                raise ValueError(f"Unknown pizza type: {kind}")


class PizzaStore:
    # Client: never names a concrete class, only the Pizza interface
    def order_pizza(self, kind: str) -> Pizza:
        pizza = SimplePizzaFactory.create_pizza(kind)
        pizza.prepare()
        pizza.bake()
        return pizza


print(PizzaStore().order_pizza("veggie").name)  # Veggie Pizza

Use it when you have a few related types (2–5), creation is straightforward, and types rarely change.

Avoid it when the switch would grow huge, creation logic is complex, or types change often. Adding a type still means editing the factory, so it doesn't satisfy Open/Closed.

Option 2: Registry-based factory#

You can remove the if/elif chain entirely by mapping names to classes. New types register themselves, so the factory's core code never changes.

from abc import ABC, abstractmethod
from typing import Dict, Type


class PaymentMethod(ABC):
    """The product interface: everything the client is allowed to know."""

    @abstractmethod
    def pay(self, amount: float) -> str: ...


class CreditCardPayment(PaymentMethod):
    def __init__(self, fee_percentage: float = 0.02):
        self.fee_percentage = fee_percentage

    def pay(self, amount: float) -> str:
        total = amount * (1 + self.fee_percentage)
        return f"Paid ${total:.2f} via Credit Card (with fee)"


class PayPalPayment(PaymentMethod):
    def pay(self, amount: float) -> str:
        return f"Paid ${amount:.2f} via PayPal"


class ApplePayPayment(PaymentMethod):
    def pay(self, amount: float) -> str:
        return f"Paid ${amount:.2f} via Apple Pay"


class PaymentFactory:
    # Name -> class lookup replaces the if/elif chain
    _registry: Dict[str, Type[PaymentMethod]] = {
        "CREDIT_CARD": CreditCardPayment,
        "PAYPAL": PayPalPayment,
        "APPLE_PAY": ApplePayPayment,
    }

    @classmethod
    def create(cls, payment_type: str) -> PaymentMethod:
        payment_cls = cls._registry.get(payment_type.upper())
        if payment_cls is None:
            raise ValueError(f"Unsupported payment method: {payment_type}")
        return payment_cls()

    @classmethod
    def register(cls, name: str, payment_cls: Type[PaymentMethod]) -> None:
        """New payment types plug in without editing this class."""
        cls._registry[name.upper()] = payment_cls


class CheckoutService:
    def process_order(self, payment_type: str, amount: float) -> str:
        # 1. Ask for the object
        payment = PaymentFactory.create(payment_type)
        # 2. Use it via the interface
        return payment.pay(amount)


print(CheckoutService().process_order("paypal", 20.0))
# Paid $20.00 via PayPal

CheckoutService has no idea what CreditCardPayment is or how it's built. To add crypto tomorrow, you write CryptoPayment and call PaymentFactory.register("CRYPTO", CryptoPayment). Neither CheckoutService nor the factory's logic changes.

Option 3: Factory Method (GoF)#

The "method" in Factory Method is an abstract creation method that subclasses override. The base class contains business logic that needs a product; each subclass decides which product.

Structure#

Participant Role
Product Interface for the objects being created
Concrete products Implementations (EmailNotification, SMSNotification)
Creator (abstract) Declares create_product() and has business logic that calls it
Concrete creators Override create_product() to return a specific product

In the notification example below, the pieces fit together like this:

  • NotificationFactory (abstract creator)
    • create_notification(): abstract, so subclasses decide
    • notify(message): concrete, and calls create_notification()
    • Subclasses:
      • EmailNotificationFactory creates an EmailNotification
      • SMSNotificationFactory creates an SMSNotification

Example#

Each concrete creator overrides one method; the business logic in notify() is written once:

from abc import ABC, abstractmethod


# Product interface
class Notification(ABC):
    @abstractmethod
    def send(self, message: str) -> None: ...


# Concrete products (these are NOT factories)
class EmailNotification(Notification):
    def send(self, message: str) -> None:
        print(f"Email: {message}")


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


# Creator: declares the factory method, and uses it
class NotificationFactory(ABC):
    @abstractmethod
    def create_notification(self) -> Notification:  # the factory method
        ...

    # Business logic is written once, against the interface
    def notify(self, message: str) -> None:
        notification = self.create_notification()
        notification.send(message)


# Concrete creators decide WHAT to create
class EmailNotificationFactory(NotificationFactory):
    def create_notification(self) -> Notification:
        return EmailNotification()


class SMSNotificationFactory(NotificationFactory):
    def create_notification(self) -> Notification:
        return SMSNotification()


if __name__ == "__main__":
    factory: NotificationFactory = EmailNotificationFactory()
    factory.notify("Hello")  # Email: Hello

    factory = SMSNotificationFactory()
    factory.notify("Hello")  # SMS: Hello

Naming tip: anything with "Factory" in the name creates objects; anything without it is the product. EmailNotificationFactory creates EmailNotification.

The pattern separates two concerns:

  1. What to create is decided by the concrete creator.
  2. How to use it is defined once in the abstract creator.

It's about deferring a decision: the base class says "I need a product" and lets subclasses choose, like a head office setting policy while regional offices pick local details.

Recipe#

  1. Define a product interface; concrete products implement it.
  2. Write an abstract creator with an abstract create_product() returning the interface, plus concrete methods that call it.
  3. Write concrete creators that override create_product().
  4. Client: c = ConcreteCreator(), then c.business_logic().

Simple Factory vs Factory Method#

The two are often confused. They differ in where the creation decision lives:

Aspect Simple Factory Factory Method
Structure One class, usually a static method or a function Abstract class with an abstract method
Decision logic Centralized (switch or registry) Spread across subclasses
Adding a type Edit the factory (or register) Add a new subclass
Open/Closed Violated (unless registry-based) Followed
Best for A few stable types Frameworks and plugins where users add types
Status Programming idiom GoF pattern

In one sentence each:

  • Simple Factory: "I have 3–4 pizza types, they rarely change, I just want creation in one place."
  • Factory Method: "I'm building a framework where users add their own transport types without touching my code."
  • Abstract Factory: "I need a whole family of related objects that must match."

Real-world examples#

The standard library and DI frameworks are full of factories:

  • iter(obj) calls obj.__iter__(): each collection returns its own iterator type, a factory method in action.
  • Alternative constructors such as datetime.fromtimestamp() and int.from_bytes(): class-method factories.
  • logging.getLogger(name): creates a logger the first time, then returns the same one.
  • sqlite3.connect(): returns a database-specific connection object.
  • Dependency-injection containers are sophisticated factories that replace much hand-written factory code in large apps.

When to use it#

A factory is worth it when:

  • You don't know the exact type until runtime.
  • You're building a library or framework and want users to plug in their own types.
  • You want creation logic in one place, or need to mock creation in tests.
  • You're following dependency inversion: depend on abstractions, not concrete classes.

When not to use it#

Skip it when:

  • Creation is simple and never varies. Just call the constructor.
  • You'd be adding classes with no real flexibility gained (over-engineering).
  • The coupling to a concrete class is actually fine.

In functional styles, a plain function that returns an object often does the job without a class hierarchy.

Pitfalls#

Watch for these mistakes:

  • Confusing the two. Simple Factory = one class with a creation method. Factory Method = abstract method overridden by subclasses.
  • Factories for everything. Only add one where there is real variation.
  • Returning concrete types. The factory should return the interface or abstract type.
  • Parameter soup. If deciding what to create needs many parameters, rethink the design.
  • Parallel hierarchies. Every new product can need a matching creator subclass, which adds classes.

Key takeaways#

  • Factories separate creating an object from using it; the client depends only on the interface.
  • A Simple Factory centralizes creation; a registry lets you add types without editing it.
  • Factory Method uses an abstract creation method that subclasses override, which satisfies Open/Closed.
  • Factory Method creates one product; Abstract Factory creates a family.
  • Don't add a factory unless creation actually varies.

Found a mistake? Tell me on LinkedIn ↗

Further reading#