backend / theory foundations / 05_creational_patterns.md

Creational Design Patterns (GoF)

7 interview angles 8 min read source

Creational Design Patterns (GoF)

The five GoF creational patterns deal with object creation mechanisms. They abstract the instantiation process, making systems independent of how their objects are created, composed, and represented.

Reference: refactoring.guru / Design Patterns — illustrated explanations of every GoF pattern; cross-reference each pattern below against the canonical descriptions there.

The five creational patterns:

Pattern Intent refactoring.guru
Singleton One instance, global access https://refactoring.guru/design-patterns/singleton
Factory Method Subclasses choose what to create https://refactoring.guru/design-patterns/factory-method
Abstract Factory Families of related products https://refactoring.guru/design-patterns/abstract-factory
Builder Step-by-step complex construction https://refactoring.guru/design-patterns/builder
Prototype Clone existing objects https://refactoring.guru/design-patterns/prototype

For structural patterns see 06_structural_patterns.md; for behavioral 07_behavioral_patterns.md.

Singleton

Intent: Ensure a class has only one instance and provide a global point of access to it. Reference: https://refactoring.guru/design-patterns/singleton

Problem: You need exactly one instance of a class (a config registry, a logger, a connection pool) shared across the application; allowing multiple instances would cause bugs or wasted resources.

Solution: Make the class itself responsible for tracking its sole instance; expose a method that returns it (creating on first access).

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        if not hasattr(self, "_initialized"):
            self.data = []
            self._initialized = True

# Thread-safe variant
from threading import Lock

class ThreadSafeSingleton:
    _instance = None
    _lock = Lock()

    def __new__(cls):
        if cls._instance is None:                # double-checked locking
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

When to use:

  • App-wide config / logger / metrics registry.
  • Hardware-bound resources (one printer driver).
  • Connection pools (though usually a pool inside a regular class is cleaner).

When NOT to use:

  • Global state harms testability. Singletons are hard to mock.
  • Hidden dependencies — callers don’t see the Singleton import; coupling is invisible.
  • Multi-threaded apps with non-locking implementations cause race conditions.

Pythonic alternatives:

  • A module-level variable IS effectively a Singleton (modules are imported once).
  • functools.lru_cache on a factory function.
  • Dependency injection instead of global access.

Common pitfalls:

  • Forgetting that __init__ runs on every Singleton() call (only __new__ is gated). Guard initialization with a _initialized flag.
  • Pickling / forking can produce multiple instances unexpectedly.
  • Testing tightly-coupled Singleton consumers is painful.

Factory Method

Intent: Define an interface for creating an object, but let subclasses decide which class to instantiate. Defers instantiation to subclasses. Reference: https://refactoring.guru/design-patterns/factory-method

Problem: A class needs to create objects, but the specific class depends on subclass / runtime conditions. Hard-coding Dog() or Cat() inside parent code blocks extension.

Solution: Replace direct constructor calls with calls to a factory method that subclasses can override.

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self) -> str: ...

class Dog(Animal):
    def speak(self): return "Woof!"

class Cat(Animal):
    def speak(self): return "Meow!"

class AnimalShelter(ABC):
    @abstractmethod
    def create_animal(self) -> Animal: ...           # the factory method

    def adopt(self):                                  # business logic uses the factory
        animal = self.create_animal()
        return f"Adopted! {animal.speak()}"

class DogShelter(AnimalShelter):
    def create_animal(self): return Dog()

class CatShelter(AnimalShelter):
    def create_animal(self): return Cat()

print(DogShelter().adopt())     # "Adopted! Woof!"

When to use:

  • Class delegates responsibility for instantiation to subclasses.
  • Different products needed at different points without if/elif/else on type.
  • Framework code creates objects whose types client code (subclasses) decides.

Difference from “factory function”:

  • The GoF Factory Method is a SUBCLASS-based pattern (each subclass overrides).
  • A “factory function” (create_user(...)) is a simpler pattern — often what Python developers actually need, called Simple Factory informally.

Common pitfalls:

  • Over-engineering: a simple factory function suffices for most cases.
  • Forced subclassing when composition would be cleaner.
  • “Factory method” everywhere just because it sounds OO.

Abstract Factory

Intent: Provide an interface for creating families of related or dependent objects without specifying their concrete classes. Reference: https://refactoring.guru/design-patterns/abstract-factory

Problem: You need to create families of objects (e.g., Windows-style Button + Checkbox + Menu vs Mac-style Button + Checkbox + Menu) where each family is internally consistent.

Solution: Define an interface for the factory that creates each product type; concrete factories implement the family.

from abc import ABC, abstractmethod

class Button(ABC):
    @abstractmethod
    def paint(self) -> str: ...

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

class WinButton(Button):
    def paint(self): return "Windows button"
class WinCheckbox(Checkbox):
    def paint(self): return "Windows checkbox"

class MacButton(Button):
    def paint(self): return "Mac button"
class MacCheckbox(Checkbox):
    def paint(self): return "Mac checkbox"

class GUIFactory(ABC):
    @abstractmethod
    def create_button(self) -> Button: ...
    @abstractmethod
    def create_checkbox(self) -> Checkbox: ...

class WinFactory(GUIFactory):
    def create_button(self): return WinButton()
    def create_checkbox(self): return WinCheckbox()

class MacFactory(GUIFactory):
    def create_button(self): return MacButton()
    def create_checkbox(self): return MacCheckbox()

def render_dialog(factory: GUIFactory):
    button = factory.create_button()
    checkbox = factory.create_checkbox()
    return [button.paint(), checkbox.paint()]

factory = WinFactory() if platform == "windows" else MacFactory()
print(render_dialog(factory))

Abstract Factory vs Factory Method:

  • Factory Method: one product type, subclass-per-product.
  • Abstract Factory: family of related products, factory-per-family.

When to use:

  • The system must be independent of how products are created/composed.
  • Multiple families of products with internal consistency requirements (Windows widgets only work with Windows look-and-feel).
  • Adding new families is more important than adding new products.

Common pitfalls:

  • Adding a new product type requires changing the factory interface AND every concrete factory. Painful at scale.
  • Over-engineering for “what if we need another DB / theme / region” that never materializes.

Builder

Intent: Separate the construction of a complex object from its representation, allowing the same construction process to create different representations. Reference: https://refactoring.guru/design-patterns/builder

Problem: A class has many optional parameters; constructors with 8+ args are unreadable. Some combinations are invalid; you need step-by-step construction with validation.

Solution: Move construction logic into a separate Builder class with fluent methods; produce the final object at the end.

from dataclasses import dataclass, field
from typing import Optional

@dataclass
class HTTPRequest:
    url: str
    method: str = "GET"
    headers: dict = field(default_factory=dict)
    body: Optional[bytes] = None
    timeout: float = 30.0
    retries: int = 0

class HTTPRequestBuilder:
    def __init__(self, url: str):
        self._req = HTTPRequest(url=url)

    def method(self, m: str):
        self._req.method = m
        return self                                  # return self for chaining

    def header(self, name: str, value: str):
        self._req.headers[name] = value
        return self

    def body(self, data: bytes):
        self._req.body = data
        return self

    def timeout(self, seconds: float):
        self._req.timeout = seconds
        return self

    def retries(self, n: int):
        self._req.retries = n
        return self

    def build(self) -> HTTPRequest:
        # validate before returning
        if self._req.method == "GET" and self._req.body:
            raise ValueError("GET requests cannot have a body")
        return self._req

req = (HTTPRequestBuilder("https://api.example.com/users")
       .method("POST")
       .header("Content-Type", "application/json")
       .body(b'{"name": "alice"}')
       .timeout(10.0)
       .retries(3)
       .build())

When to use:

  • Constructor takes many optional arguments.
  • Construction has multiple steps; some require validation between steps.
  • Same construction code should produce different representations (Director pattern).

Pythonic alternatives:

  • dataclasses with defaults (often sufficient).
  • Keyword-only arguments with * separator.
  • Named tuples for immutable cases.
  • The Builder pattern shines when validation between steps or fluent chaining benefits readability significantly.

Common pitfalls:

  • Building a Builder for a 2-argument class — pure ceremony.
  • Forgetting that intermediate state may be invalid; the Builder produces the final, validated object.

Prototype

Intent: Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype. Reference: https://refactoring.guru/design-patterns/prototype

Problem: You need to create many objects similar to an existing one. Constructing from scratch is expensive or requires data not accessible at creation time.

Solution: Have objects implement a clone() method that produces a deep copy of themselves.

from copy import deepcopy
from typing import Any

class Prototype:
    def clone(self) -> "Prototype":
        return deepcopy(self)

class Document(Prototype):
    def __init__(self, title: str, content: dict, formatting: dict):
        self.title = title
        self.content = content
        self.formatting = formatting

    def __repr__(self):
        return f"Document(title={self.title!r}, content={self.content})"

template = Document(
    title="Template",
    content={"sections": ["intro", "body", "conclusion"]},
    formatting={"font": "Arial", "size": 12},
)

# Each report starts from the template, then customizes
report1 = template.clone()
report1.title = "Q1 Report"
report1.content["sections"][0] = "Q1 intro"

report2 = template.clone()
report2.title = "Q2 Report"

assert report1.content is not report2.content       # truly independent

When to use:

  • Object creation is expensive (parsing config, opening connections, computing initial state).
  • You need objects whose exact class isn’t known at compile time.
  • You want to avoid the overhead of factories for simple variations.

Pythonic notes:

  • copy.deepcopy is the standard implementation.
  • Watch for non-copyable members (file handles, locks, threads); implement __deepcopy__ if needed.
  • For dataclasses: dataclasses.replace(obj, **changes) returns a modified copy.

Common pitfalls:

  • Shallow vs deep copy confusion — see ../02_python_core/17_shallow_vs_deep_copy.md.
  • Cloning objects with non-clonable state (open sockets, threads).
  • Forgetting that clone() returns a new instance; treating it like a reference.

Choosing among creational patterns

Situation Pattern
One instance, app-wide Singleton (or just use a module)
Subclass decides which object to create Factory Method
Family of related products, internally consistent Abstract Factory
Complex construction with many options Builder
Copy expensive-to-construct objects Prototype

When you don’t need any of these — a plain Foo(...) constructor or a top-level def create_foo() function is simpler. Don’t apply patterns dogmatically.

Common interview confusions

  • “Factory Method and Abstract Factory are the same.” — Factory Method: subclassing to vary ONE product. Abstract Factory: composition to vary a FAMILY of products.
  • “Singleton is always bad.” — global state is risky, but well-bounded use cases exist (logger, config). The anti-Singleton stance is about over-application, not literal prohibition.
  • “Builder is for immutable objects.” — not specifically; Builder is for step-by-step construction. Often used with immutable results, but mutable result objects work fine too.

Interview angle

  • “What are the GoF creational patterns?” — five patterns dealing with object creation: Singleton (one instance), Factory Method (subclass-decided), Abstract Factory (family of products), Builder (step-by-step complex construction), Prototype (clone existing).
  • “Factory Method vs Abstract Factory?” — Factory Method creates one product, with variation via subclassing. Abstract Factory creates a family of related products, with variation via concrete factory implementations.
  • “How do you implement Singleton in Python? What’s the Pythonic alternative?”__new__ with class-level _instance (plus thread-safety lock if needed). Pythonic alternative: a module-level variable (modules are imported once) or dependency injection.
  • “When is Builder overkill?” — when constructor + dataclass with defaults suffices. Builder pays off with many optional args, mid-construction validation, or fluent chaining significantly improves readability.
  • “What does Prototype solve?” — expensive-to-construct objects you need many of. Clone an existing instance instead of running the full constructor. copy.deepcopy is the standard implementation in Python.
  • “Why is Singleton hard to test?” — global state persists across tests; one test’s mutation affects the next. Hidden dependency on the Singleton instance makes mocking awkward. Prefer DI for testability.
  • “Pool pattern — is it GoF?” — no. It’s an object-pool pattern (Fowler / others) often grouped with creational patterns informally. Useful for expensive resources (DB connections); but not in the original 23.