WierdX — Programming Reference All tutorials →
Developer reference · Practical tutorials · CS fundamentals
Software Engineering

Creational Design Patterns Explained: Factory, Builder, Singleton

Creational patterns answer the question: who is responsible for constructing an object, and how much does the caller need to know about what it is getting? Factory Method, Abstract Factory, Builder, and Singleton each address a distinct construction problem, and each one is also over-applied when the simpler solution is a direct constructor call.

Published June 25, 2026

The Gang of Four book (Gamma, Helm, Johnson, Vlissides, 1994) catalogued 23 patterns. The creational group covers five: Singleton, Factory Method, Abstract Factory, Builder, and Prototype. They remain relevant not because every codebase needs all five, but because the problems they solve — decoupling construction from use, managing complex initialization sequences, controlling instance count — are problems that recur in real software.

Each pattern comes with a cost: an extra layer of indirection, more classes, and sometimes obscured control flow. The right time to reach for a pattern is when you already feel the pain it solves, not when you are designing in the abstract.

Factory Method

The Factory Method pattern defines an interface for creating an object but lets subclasses decide which concrete class to instantiate. The caller works with the interface; the choice of implementation is delegated to a method that can be overridden.

The problem it solves: you have code that needs to create objects, but the exact class to create depends on context that the caller should not need to know about.

from abc import ABC, abstractmethod

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

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}")

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

def notification_factory(channel: str) -> Notification:
    registry = {
        "email": EmailNotification,
        "sms":   SMSNotification,
        "push":  PushNotification,
    }
    cls = registry.get(channel)
    if cls is None:
        raise ValueError(f"Unknown channel: {channel}")
    return cls()

# The caller does not know which class it received
notifier = notification_factory("email")
notifier.send("Your order shipped.")

The factory is the single place where the channel string maps to a concrete class. Adding a new channel means adding one line to the registry dict, not modifying every caller. This satisfies the open/closed principle: the factory is open for extension (new channels) without modifying existing code.

When not to use it: if you only ever create one type of object and that is unlikely to change, a direct constructor call is clearer. The factory adds indirection that is not paying for anything.

Abstract Factory

Abstract Factory extends the idea: instead of a factory that creates one type of object, it is a factory that creates a family of related objects. The guarantee is that objects produced by the same factory are compatible with each other.

from abc import ABC, abstractmethod

# Abstract products
class Button(ABC):
    @abstractmethod
    def render(self) -> str: ...

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

# Concrete products -- two families: Web and Mobile
class WebButton(Button):
    def render(self) -> str: return "<button>"

class WebCheckbox(Checkbox):
    def render(self) -> str: return "<input type='checkbox'>"

class MobileButton(Button):
    def render(self) -> str: return "NativeButton()"

class MobileCheckbox(Checkbox):
    def render(self) -> str: return "NativeCheckbox()"

# Abstract factory
class UIFactory(ABC):
    @abstractmethod
    def create_button(self) -> Button: ...

    @abstractmethod
    def create_checkbox(self) -> Checkbox: ...

# Concrete factories -- each produces a consistent family
class WebUIFactory(UIFactory):
    def create_button(self) -> Button: return WebButton()
    def create_checkbox(self) -> Checkbox: return WebCheckbox()

class MobileUIFactory(UIFactory):
    def create_button(self) -> Button: return MobileButton()
    def create_checkbox(self) -> Checkbox: return MobileCheckbox()

# Client code uses only the abstract factory and abstract products
def render_form(factory: UIFactory):
    button   = factory.create_button()
    checkbox = factory.create_checkbox()
    return button.render(), checkbox.render()

The benefit: render_form is completely decoupled from whether it is rendering for web or mobile. Swapping the factory at the call site changes the entire family of components consistently. This is useful when the constraint "all components in a session must be from the same family" is real and enforced by the type system.

Builder

Builder separates the construction of a complex object from its representation. It is the right pattern when an object has many optional parameters, and direct constructor calls become unreadable when most of them are filled in.

Consider an HTTP request object with dozens of optional fields — headers, timeout, auth, body, query params, TLS settings. A constructor with fifteen parameters is unusable; keyword arguments help but still require knowing which are required. Builder makes construction fluent and self-documenting:

class HttpRequest:
    def __init__(self, method, url, headers, body, timeout, auth, verify_ssl):
        self.method     = method
        self.url        = url
        self.headers    = headers
        self.body       = body
        self.timeout    = timeout
        self.auth       = auth
        self.verify_ssl = verify_ssl

class HttpRequestBuilder:
    def __init__(self, method: str, url: str):
        self._method     = method
        self._url        = url
        self._headers    = {}
        self._body       = None
        self._timeout    = 30
        self._auth       = None
        self._verify_ssl = True

    def header(self, key: str, value: str) -> "HttpRequestBuilder":
        self._headers[key] = value
        return self

    def body(self, data: bytes) -> "HttpRequestBuilder":
        self._body = data
        return self

    def timeout(self, seconds: int) -> "HttpRequestBuilder":
        self._timeout = seconds
        return self

    def bearer_auth(self, token: str) -> "HttpRequestBuilder":
        self._auth = ("Bearer", token)
        return self

    def build(self) -> HttpRequest:
        return HttpRequest(
            method=self._method, url=self._url,
            headers=self._headers, body=self._body,
            timeout=self._timeout, auth=self._auth,
            verify_ssl=self._verify_ssl
        )

# Fluent, readable construction
request = (
    HttpRequestBuilder("POST", "https://api.example.com/orders")
    .header("Content-Type", "application/json")
    .bearer_auth("tok_abc123")
    .timeout(10)
    .body(b'{"product_id": 42, "qty": 1}')
    .build()
)

Each method returns self, enabling method chaining. The build() call validates that all required fields are present and produces the final immutable object. If build() raises on missing required fields, construction errors surface at the call site rather than as AttributeErrors later.

Python's dataclasses with field(default=...) and libraries like Pydantic handle many of the same cases more concisely. Reach for an explicit Builder when the construction logic is complex enough to warrant its own validation and the chained API genuinely improves readability.

Singleton

Singleton ensures that a class has only one instance and provides a global access point to it. It is the most contested of the creational patterns because it couples every consumer to the global state it holds and makes testing difficult.

class ConfigurationManager:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._config = {}
        return cls._instance

    def load(self, path: str) -> None:
        with open(path) as f:
            import json
            self._config = json.load(f)

    def get(self, key: str, default=None):
        return self._config.get(key, default)

# Both variables point to the same object
a = ConfigurationManager()
b = ConfigurationManager()
assert a is b   # True

The legitimate use cases are genuinely scarce: a logger that writes to a single file, a connection pool that must not be duplicated, a registry that accumulates entries from across the codebase. The pattern becomes harmful when used as a convenient global variable disguised as an object.

The testing problem is concrete: a test that modifies a Singleton's state affects every subsequent test in the process unless you add teardown logic that resets it. This is global mutable state by another name.

The modern alternative for most Singleton use cases is dependency injection: create the shared instance once in a composition root (your main function or application startup), and pass it explicitly to every component that needs it. The instance is still shared, but the dependency is explicit, testable, and replaceable.

# Instead of Singleton -- inject the shared instance explicitly
def create_app():
    config  = ConfigurationManager()
    config.load("/etc/myapp/config.json")

    db_pool = DatabasePool(config.get("db_url"), max_connections=20)
    cache   = RedisCache(config.get("redis_url"))

    return Application(db_pool=db_pool, cache=cache, config=config)

Choosing the right pattern

The creational patterns cover a spectrum from "who chooses the class" (Factory Method, Abstract Factory) to "how complex objects are assembled" (Builder) to "how many instances exist" (Singleton). The signal that you need one is a specific pain: a proliferating if/elif chain that decides which class to instantiate, a constructor with ten parameters most of which are optional, incompatible object families being mixed accidentally.

If you are reaching for a pattern before you feel the pain, you are adding complexity speculatively. Add it when the code tells you it needs the structure, and you will find the pattern fits cleanly rather than feeling forced.