Skip to content

Creational Design Patterns#

Start here

Read the Design Patterns overview for interview mapping (LRU, Iterator, etc.) and how this section relates to Challenge Yourself.

Creational patterns deal with object creation — they abstract instantiation so your code depends on interfaces, not concrete classes. All examples below are Python-first, complete, and runnable.


Singleton Pattern#

Purpose: Guarantee exactly one instance of a class and expose a single global access point.

When to use:

  • Shared resources: logging, configuration, connection pools, caches
  • You need lazy initialization (create on first use)
  • Multiple components must coordinate through one object
from __future__ import annotations

import threading
from datetime import datetime


class Logger:
    _instance: Logger | None = None
    _lock = threading.Lock()

    def __new__(cls) -> Logger:
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    cls._instance._initialized = False
        return cls._instance

    def __init__(self) -> None:
        if self._initialized:
            return
        self._initialized = True

    def info(self, message: str) -> None:
        self._log("INFO", message)

    def warn(self, message: str) -> None:
        self._log("WARN", message)

    def error(self, message: str) -> None:
        self._log("ERROR", message)

    def _log(self, level: str, message: str) -> None:
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        print(f"{timestamp} [{level}]: {message}")


if __name__ == "__main__":
    a = Logger()
    b = Logger()
    print(a is b)  # True — same instance
    a.info("Application started")
    b.warn("Cache warming")
    a.error("Disk nearly full")

Explanation:

  1. __new__ controls instance creation; __init__ runs on every call, so guard with _initialized.
  2. Double-checked locking (_lock + second if) avoids creating multiple instances under concurrency.
  3. Callers use Logger() or a module-level instance — both resolve to the same object.
  4. Private state (_log) centralizes formatting; public methods expose severity levels.

Interview note: In Python, a module-level object (logger = Logger()) is often the idiomatic singleton. Know __new__ for OOP interviews; mention thread safety when asked about multi-threaded servers.


Builder Pattern#

Purpose: Construct complex objects step by step, separating the construction process from the final representation.

When to use:

  • Many optional fields (avoid telescoping constructors)
  • You want immutable products after construction
  • The same building steps produce different representations
from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True)
class Meal:
    main_dish: str
    side_dish: str
    drink: str
    dessert: str = "Default Dessert"
    appetizer: str = "Default Appetizer"

    def summary(self) -> str:
        return (
            f"Main: {self.main_dish}\n"
            f"Side: {self.side_dish}\n"
            f"Drink: {self.drink}\n"
            f"Dessert: {self.dessert}\n"
            f"Appetizer: {self.appetizer}"
        )


class MealBuilder:
    def __init__(self, main_dish: str, side_dish: str, drink: str) -> None:
        self._main_dish = main_dish
        self._side_dish = side_dish
        self._drink = drink
        self._dessert = "Default Dessert"
        self._appetizer = "Default Appetizer"

    def set_dessert(self, dessert: str) -> MealBuilder:
        self._dessert = dessert
        return self

    def set_appetizer(self, appetizer: str) -> MealBuilder:
        self._appetizer = appetizer
        return self

    def build(self) -> Meal:
        return Meal(
            main_dish=self._main_dish,
            side_dish=self._side_dish,
            drink=self._drink,
            dessert=self._dessert,
            appetizer=self._appetizer,
        )


if __name__ == "__main__":
    full_meal = (
        MealBuilder("Grilled Salmon", "Quinoa", "Sparkling Water")
        .set_dessert("Cheesecake")
        .set_appetizer("Soup")
        .build()
    )
    simple_meal = MealBuilder("Pasta", "Salad", "Juice").build()

    print("Full Meal:\n", full_meal.summary(), sep="")
    print("\nSimple Meal:\n", simple_meal.summary(), sep="")

Explanation:

  1. Meal is the product — frozen dataclass keeps it immutable after build().
  2. MealBuilder holds required fields in __init__ and optional fields with defaults.
  3. Fluent setters (set_dessert, set_appetizer) return self for chaining.
  4. build() validates nothing extra here but is the single place to enforce invariants before returning Meal.

Interview note: Contrast with @dataclass defaults or **kwargs. Builder shines when construction has multiple stages or validation (e.g., "dessert requires main dish").


Factory Method Pattern#

Purpose: Define an interface for creating an object, but let subclasses decide which concrete class to instantiate.

When to use:

  • A class cannot anticipate the exact type of objects it must create
  • Subclasses should control which product gets built
  • You want to decouple client code from concrete product classes
from __future__ import annotations

from abc import ABC, abstractmethod


class Document(ABC):
    @abstractmethod
    def display_type(self) -> None:
        pass


class PDFDocument(Document):
    def display_type(self) -> None:
        print("Creating a PDF Document")


class WordDocument(Document):
    def display_type(self) -> None:
        print("Creating a Word Document")


class HTMLDocument(Document):
    def display_type(self) -> None:
        print("Creating an HTML Document")


class DocumentCreator(ABC):
    @abstractmethod
    def create_document(self) -> Document:
        pass

    def export(self) -> None:
        document = self.create_document()
        document.display_type()


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


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


class HTMLCreator(DocumentCreator):
    def create_document(self) -> Document:
        return HTMLDocument()


def get_creator(document_type: str) -> DocumentCreator:
    creators: dict[str, type[DocumentCreator]] = {
        "pdf": PDFCreator,
        "word": WordCreator,
        "html": HTMLCreator,
    }
    key = document_type.lower()
    if key not in creators:
        raise ValueError(f"Unknown document type: {document_type}")
    return creators[key]()


if __name__ == "__main__":
    for doc_type in ("pdf", "word", "html"):
        get_creator(doc_type).export()

Explanation:

  1. Document is the product interface; concrete documents implement display_type.
  2. DocumentCreator declares create_document() — the factory method — and uses it in export().
  3. Each subclass (PDFCreator, etc.) overrides create_document() to return its product.
  4. get_creator selects the right creator at runtime; clients call export() without knowing concrete classes.

Interview note: Do not confuse with Simple Factory (one function with if/elif) or Abstract Factory (families of related products). Factory Method = one product, subclass decides the type.


Abstract Factory Pattern#

Purpose: Provide an interface for creating families of related objects without specifying their concrete classes.

When to use:

  • Products must be compatible within a family (e.g., macOS button + macOS checkbox)
  • You need to switch entire product families at runtime
  • You want to hide concrete class names from client code
from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass


class Button(ABC):
    @abstractmethod
    def render(self) -> str:
        pass


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


@dataclass
class WindowsButton(Button):
    def render(self) -> str:
        return "[Windows Button]"


@dataclass
class WindowsCheckbox(Checkbox):
    def render(self) -> str:
        return "[Windows Checkbox]"


@dataclass
class MacButton(Button):
    def render(self) -> str:
        return "(Mac Button)"


@dataclass
class MacCheckbox(Checkbox):
    def render(self) -> str:
        return "(Mac Checkbox)"


class GUIFactory(ABC):
    @abstractmethod
    def create_button(self) -> Button:
        pass

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


class WindowsFactory(GUIFactory):
    def create_button(self) -> Button:
        return WindowsButton()

    def create_checkbox(self) -> Checkbox:
        return WindowsCheckbox()


class MacFactory(GUIFactory):
    def create_button(self) -> Button:
        return MacButton()

    def create_checkbox(self) -> Checkbox:
        return MacCheckbox()


class Application:
    def __init__(self, factory: GUIFactory) -> None:
        self.button = factory.create_button()
        self.checkbox = factory.create_checkbox()

    def render(self) -> None:
        print(self.button.render())
        print(self.checkbox.render())


if __name__ == "__main__":
    for factory in (WindowsFactory(), MacFactory()):
        app = Application(factory)
        app.render()
        print()

Explanation:

  1. Button and Checkbox are separate product interfaces — the family of related objects.
  2. GUIFactory declares factory methods for every product in the family.
  3. WindowsFactory and MacFactory each return matching concrete products (no mixing Windows button with Mac checkbox).
  4. Application depends only on GUIFactory; swapping the factory swaps the entire UI theme.

Interview note: Abstract Factory is "Factory Method × N products." Common follow-up: adding a new product (e.g., Textbox) forces changes to every concrete factory — trade-off of the pattern.


Prototype Pattern#

Purpose: Create new objects by cloning an existing instance instead of building from scratch.

When to use:

  • Object construction is expensive or complex
  • You need many objects that differ only slightly from a template
  • You want to avoid subclassing just to vary configuration
from __future__ import annotations

import copy
from abc import ABC, abstractmethod
from dataclasses import dataclass, field


@dataclass
class Character(ABC):
    name: str
    health: int
    attack_power: int
    defense: int

    @abstractmethod
    def display(self) -> None:
        pass

    def clone(self) -> Character:
        return copy.deepcopy(self)


@dataclass
class Warrior(Character):
    role: str = field(default="Warrior", init=False)

    def display(self) -> None:
        print(
            f"{self.role} - Name: {self.name}, Health: {self.health}, "
            f"Attack: {self.attack_power}, Defense: {self.defense}"
        )


@dataclass
class Mage(Character):
    role: str = field(default="Mage", init=False)

    def display(self) -> None:
        print(
            f"{self.role} - Name: {self.name}, Health: {self.health}, "
            f"Attack: {self.attack_power}, Defense: {self.defense}"
        )


class CharacterRegistry:
    def __init__(self) -> None:
        self._prototypes: dict[str, Character] = {}

    def register(self, key: str, prototype: Character) -> None:
        self._prototypes[key] = prototype

    def spawn(self, key: str) -> Character:
        if key not in self._prototypes:
            raise KeyError(f"Unknown prototype: {key}")
        return self._prototypes[key].clone()


if __name__ == "__main__":
    registry = CharacterRegistry()
    registry.register("warrior", Warrior("Aragorn", 100, 25, 15))

    original = registry.spawn("warrior")
    cloned = registry.spawn("warrior")
    cloned.health = 80
    cloned.attack_power = 30
    cloned.defense = 10

    print("Original:")
    original.display()
    print("\nCloned (modified):")
    cloned.display()
    print(f"\nSame object? {original is cloned}")  # False

Explanation:

  1. Character defines the interface; clone() uses copy.deepcopy for a independent copy.
  2. Concrete classes (Warrior, Mage) hold state and implement display().
  3. CharacterRegistry stores prototype templates keyed by name — the prototype manager.
  4. spawn() clones a template; callers tweak the copy without affecting the original or the registry entry.

Interview note: Python's copy.copy vs copy.deepcopy matters when prototypes hold mutable fields (lists, dicts). Mention registry pattern for game entities, config presets, or document templates.