Exploring Design Patterns: A Journey through Factory Methods

What?

Factory Method is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.

A factory is a creational design pattern that delegates object creation to a separate method or class, instead of calling a constructor directly with new/ClassName().

Instead of the client code knowing exactly which class to instantiate, it asks a factory for an object, and the factory decides which concrete class to build.

Why object creation needs its own pattern

In most object-oriented programs, creating an object is trivial: you call a constructor, and you’re done. But as a codebase grows, the decision of which class to instantiate often becomes conditional, configuration-driven, or dependent on runtime context. When object construction logic gets tangled into business logic, code becomes brittle — every time you add a new type, you have to hunt down every place that constructs objects and update a chain of if/elif statements.

The Factory pattern solves this by extracting the “what to create” decision into a dedicated place. Client code asks a factory for an object and receives something that satisfies an expected interface, without needing to know the concrete class, the constructor arguments, or the construction logic involved. This single idea shows up in three related but distinct forms: the Simple Factory, the Factory Method, and the Abstract Factory. They sit on a spectrum of increasing flexibility and complexity, and choosing the right one matters.

The core problem

Suppose you’re building a notification system that can send messages via email, SMS, or push notification. A naive approach scatters conditionals everywhere:

Naive approach

def send_notification(kind: str, message: str):
    if kind == "email":
        sender = EmailSender()
    elif kind == "sms":
        sender = SMSSender()
    elif kind == "push":
        sender = PushSender()
    else:
        raise ValueError(f"Unknown notification kind: {kind}")
    sender.send(message)
Python

This works until the if/elif chain starts appearing in five other files — the test suite, the admin dashboard, the retry handler — each one needing to stay in sync whenever a new channel is added. That duplication and coupling is the exact smell the Factory pattern is designed to remove.

Simple Factory: centralising the conditional

The Simple Factory isn’t one of the original Gang of Four patterns, but it’s the most commonly used variant in practice because it’s the easiest first step. You move the conditional logic into one function or class whose only job is creation:

from abc import ABC, abstractmethod

class Notification(ABC):
    @abstractmethod
    def send(self, message): pass


class EmailNotification(Notification):
    def send(self, message):
        print(f"Email: {message}")


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


class PushNotification(Notification):
    def send(self, message):
        print(f"Push: {message}")


class NotificationFactory:
    @staticmethod
    def create(notif_type: str) -> Notification:
        if notif_type == "email":
            return EmailNotification()
        elif notif_type == "sms":
            return SMSNotification()
        elif notif_type == "push":
            return PushNotification()
        raise ValueError(f"Unknown type: {notif_type}")
Python

Now client code is simple and decoupled from concrete classes:

notifier = NotificationFactory.create("sms")
notifier.send("Your order has shipped")
# SMS: Your order has shipped
Python

Adding a new channel means touching the factory’s registry, not every call site. That’s the whole value proposition at this level: a single, well-defined seam for construction logic.

Naive approach and Simple Factory look similar

On the surface, they look identical. Both have an if/elif block choosing a class. The difference isn’t in the syntax; it’s in where that block lives and who depends on it.

Nothing about the branching logic itself got smarter. The if/elif is still there — Simple Factory doesn’t eliminate it; it just relocates it.

Naive approach — the if/elif lives inside the client code

# Somewhere in your checkout view
def process_order(notif_type, message):
    if notif_type == "email":
        notifier = EmailNotification()
    elif notif_type == "sms":
        notifier = SMSNotification()
    elif notif_type == "push":
        notifier = PushNotification()
    notifier.send(message)
Python

# Somewhere else in your signup view
def send_welcome(notif_type, message):
    if notif_type == "email":
        notifier = EmailNotification()
    elif notif_type == "sms":
        notifier = SMSNotification()
    elif notif_type == "push":
        notifier = PushNotification()
    notifier.send(message)
Python

The if/elif is duplicated everywhere you need to create a notifier. Every piece of client code that needs a notification must import EmailNotification, SMSNotification, PushNotification and know the full list of types that exist.

If you add SlackNotification tomorrow, you have to find and edit every one of these scattered blocks.

Simple Factory — the if/elif lives in exactly one place

class NotificationFactory:
    @staticmethod
    def create(notif_type):
        if notif_type == "email":
            return EmailNotification()
        elif notif_type == "sms":
            return SMSNotification()
        elif notif_type == "push":
            return PushNotification()
Python

# checkout view
def process_order(notif_type, message):
    notifier = NotificationFactory.create(notif_type)
    notifier.send(message)
Python

# signup view
def send_welcome(notif_type, message):
    notifier = NotificationFactory.create(notif_type)
    notifier.send(message)
Python

The if/elif still exists — but now it exists once, in one file. The checkout view and signup view no longer need to import EmailNotification or know anything about the full list of notification types. They just ask the factory for one.

If you add SlackNotification tomorrow, you edit one file — NotificationFactory. Every client that uses it benefits automatically, with zero changes to their own code.

Factory Method: letting subclasses decide

The Factory Method pattern, one of the original 23 GoF patterns, takes this further by making creation an overridable method rather than a static lookup table. The idea is that a base class defines the skeleton of an operation, but defers the decision of which concrete product to instantiate to subclasses.

This matters when the creation logic is tightly coupled to a larger algorithm that varies by context. Consider a document processing pipeline:

from abc import ABC, abstractmethod


# ---------- Product ----------
class Document(ABC):
    """Abstract Product. Defines the interface every document must implement."""

    @abstractmethod
    def render(self) -> str:
        ...

    @abstractmethod
    def extension(self) -> str:
        ...


# ---------- ConcreteProduct ----------
class PDFDocument(Document):
    def render(self) -> str:
        return "Rendering PDF content"

    def extension(self) -> str:
        return ".pdf"


class WordDocument(Document):
    def render(self) -> str:
        return "Rendering Word content"

    def extension(self) -> str:
        return ".docx"


class ExcelDocument(Document):
    def render(self) -> str:
        return "Rendering Excel content"

    def extension(self) -> str:
        return ".xlsx"


# ---------- Creator ----------
class DocumentCreator(ABC):
    """
    Declares the factory method, which subclasses override to
    produce an object of a ConcreteProduct class.

    The Creator's other methods (process, save, etc.) are written
    purely against the abstract Product interface — they NEVER
    reference a concrete class directly. This is the core discipline
    of Factory Method: the Creator depends only on what the factory
    method promises to return, not on how it's built.
    """

    @abstractmethod
    def create_document(self) -> Document:
        """The factory method. Must be overridden by every ConcreteCreator."""
        ...

    # ---- Everything below is shared logic, untouched by subclasses ----

    def process(self) -> None:
        document = self.create_document()      # delegate creation
        print(document.render())
        self._apply_watermark(document)
        self._log_audit(document)

    def _apply_watermark(self, document: Document) -> None:
        print(f"Applying watermark to {document.extension()} file")

    def _log_audit(self, document: Document) -> None:
        print(f"Logged creation of {document.extension()} document")


# ---------- ConcreteCreator ----------
class PDFCreator(DocumentCreator):
    def create_document(self) -> Document:
        return PDFDocument()


class WordCreator(DocumentCreator):
    def create_document(self) -> Document:
        return WordDocument()


class ExcelCreator(DocumentCreator):
    def create_document(self) -> Document:
        return ExcelDocument()
Python

Client code — depends only on the abstract Creator

def generate_report(creator: DocumentCreator) -> None:
    """
    This function never imports PDFDocument, WordDocument, or
    ExcelDocument. It only knows about DocumentCreator and the
    Document interface — the entire point of the pattern.
    """
    creator.process()


generate_report(PDFCreator())
# Rendering PDF content
# Applying watermark to .pdf file
# Logged creation of .pdf document

generate_report(WordCreator())
# Rendering Word content
# Applying watermark to .docx file
# Logged creation of .docx document

generate_report(ExcelCreator())
# Rendering Excel content
# Applying watermark to .xlsx file
# Logged creation of .xlsx document
Python

Three things distinguish it:

The factory method (create_document) is abstract and lives on the Creator itself, not on a separate standalone factory class. Creation is a responsibility of the class hierarchy, not delegated outward to a helper object.

The Creator contains real shared behaviour (process, _apply_watermark, _log_audit) that depends on the product being created, but is written once and inherited by every subclass. This is the Template Method relationship we talked about — process() is the fixed skeleton, create_document() is the one varying step.

Adding a new document type means subclassing DocumentCreator, not editing an if/elif anywhere:

class MarkdownDocument(Document):
    def render(self) -> str:
        return "Rendering Markdown content"

    def extension(self) -> str:
        return ".md"


class MarkdownCreator(DocumentCreator):
    def create_document(self) -> Document:
        return MarkdownDocument()
Python

generate_report(MarkdownCreator())
# Rendering Markdown content
# Applying watermark to .md file
# Logged creation of .md document
Python

No existing class was touched. This is the real test of whether you’ve implemented Factory Method correctly — if adding a new product type ever requires editing an existing if/elif block, you’ve drifted back into Simple Factory territory, which is a perfectly fine pattern, just a different one from what the GoF book describes.

Is Simple Factory better than the real factory method?

For a small, everyday problem, Simple Factory — or honestly even simpler, just a dictionary of functions — gets you the same benefit with way less code.

Factory Method is a specialised tool for a specific job (building extensible frameworks/libraries), not a general upgrade over Simple Factory. Using it when you don’t need it is like bringing a toolbox full of speciality wrenches to tighten one screw you could’ve done with your fingers.


About Puneet Verma

Puneet Verma is a software developer specialising in backend architecture, Dynamic Programming, and SaaS solutions. He focuses on building optimised, scalable applications and sharing deep-dive technical tutorials to help developers master complex algorithmic patterns.

Leave a Comment