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, 2026The 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.