Francesco Sannicola

Machine Learning | Software Engineering

./articles/ Software Engineering Essentials: Creational Design Patterns

Last modified:

Creational design patterns organize how objects come into existence: which implementation is selected, how a product is assembled or copied, and when an instance is shared. They move those decisions behind deliberate construction boundaries instead of scattering concrete constructor calls and lifecycle assumptions across client code.

This chapter covers all five Creational patterns in the Gang of Four catalog. Each section starts with a concrete design problem, follows the creation flow, and supplies independent Python and Java examples with matching demo output. For the other GoF categories, explore Structural Design Patterns, focused on composition, and Behavioral Design Patterns, focused on object collaboration.

Project source: OOP Design Patterns Handbook: Creational Patterns.

Run each Python example independently with Python 3.10 or later; only the standard library is required. Each Java example is a separate source file named in its first comment. Compile and run its public class, for example javac -encoding UTF-8 FactoryMethodDemo.java followed by java FactoryMethodDemo. The Java examples target Java 8 or later and use only JDK APIs. Do not combine the snippets into one file.

Repository verification: the linked source could not be retrieved during preparation. These are independently tested Python reference examples, not verified reproductions of the repository's APIs or naming. No specific upstream discrepancy is asserted without inspecting that source.

Quick Reference

PatternCore IntentKey Benefit
Factory MethodDelegate a creation hook to subclassesExtend a shared workflow with a different product
Abstract FactoryCreate a family of related productsKeep the family selection consistent across product types
BuilderSeparate stepwise assembly from the finished objectName construction steps and control publication
PrototypeCopy a configured object through a cloning contractReuse runtime presets without reconstructing them in client code
SingletonControl one instance and a shared access pointMake identity and runtime scope explicit

The diagrams trace creation and ownership, not every method or inheritance relationship. On narrow screens, diagrams scroll horizontally to keep labels readable. Code and output use the same expandable component as the Behavioral chapter and remain readable if JavaScript is unavailable.

1. Factory Method

Intent

Factory Method defines a creation hook in a superclass and lets subclasses decide which concrete product to return. The superclass can then run its normal workflow against the product interface rather than a hard-coded class.

The Problem It Solves

A logistics service starts with road deliveries. If its planning workflow constructs Truck directly, adding sea delivery forces shipping-specific decisions into that workflow. The shared dispatch process should remain stable while subclasses supply the appropriate transport.

Structure

Factory Method delegates product creation The inherited plan_delivery workflow calls a factory method. RoadLogistics creates a Truck and SeaLogistics creates a Ship. The product is returned to the workflow and used through Transport. plan_delivery() inherited workflow create_transport() overridden creation hook Transport Truck or Ship 1. delegate 2. create 3. Return the product; the workflow calls deliver()
Changing the creator subclass changes the product, not the delivery workflow.
From Product Selection to Delivery
  1. The application selects a concrete logistics service at its configuration boundary.
  2. The inherited planning method calls the creation hook on that service.
  3. The returned product performs delivery through the common interface.

The demo prints messages instead of contacting a shipping provider. Its dispatch approval is just a visible shared step, not a real business validation.

Python Implementation

from abc import ABC, abstractmethod


class Transport(ABC):
    @abstractmethod
    def deliver(self, parcel: str) -> str:
        pass


class Truck(Transport):
    def deliver(self, parcel: str) -> str:
        return f"Truck delivers {parcel} by road"


class Ship(Transport):
    def deliver(self, parcel: str) -> str:
        return f"Ship delivers {parcel} by sea"


class Logistics(ABC):
    @abstractmethod
    def create_transport(self) -> Transport:
        pass

    def plan_delivery(self, parcel: str) -> str:
        transport = self.create_transport()
        return f"Dispatch approved: {transport.deliver(parcel)}"


class RoadLogistics(Logistics):
    def create_transport(self) -> Transport:
        return Truck()


class SeaLogistics(Logistics):
    def create_transport(self) -> Transport:
        return Ship()


if __name__ == "__main__":
    for logistics in (RoadLogistics(), SeaLogistics()):
        print(logistics.plan_delivery("books"))

Java Implementation

// FactoryMethodDemo.java

interface Transport {
    String deliver(String parcel);
}

final class Truck implements Transport {
    public String deliver(String parcel) {
        return "Truck delivers " + parcel + " by road";
    }
}

final class Ship implements Transport {
    public String deliver(String parcel) {
        return "Ship delivers " + parcel + " by sea";
    }
}

abstract class Logistics {
    protected abstract Transport createTransport();

    public String planDelivery(String parcel) {
        Transport transport = createTransport();
        return "Dispatch approved: " + transport.deliver(parcel);
    }
}

final class RoadLogistics extends Logistics {
    @Override
    protected Transport createTransport() { return new Truck(); }
}

final class SeaLogistics extends Logistics {
    @Override
    protected Transport createTransport() { return new Ship(); }
}

public class FactoryMethodDemo {
    public static void main(String[] args) {
        Logistics[] services = {new RoadLogistics(), new SeaLogistics()};
        for (Logistics logistics : services) {
            System.out.println(logistics.planDelivery("books"));
        }
    }
}

Output

Dispatch approved: Truck delivers books by road
Dispatch approved: Ship delivers books by sea

Factory Method vs. a Simple Factory

A helper such as make_transport(kind) that switches on a string can centralize construction, but that alone is not the GoF Factory Method pattern. Here, creation is a polymorphic extension point used by an inherited workflow. A conditional may still select the creator at startup; the pattern removes product-specific construction from the workflow, not every conditional from the application.

A factory method need not allocate a new object on every call: it may return a cached or pooled product if the ownership contract permits it. This example deliberately creates a fresh transport each time. The main cost is an extra creator hierarchy, which is unnecessary when product creation is simple and unlikely to vary.

When to Use It

2. Abstract Factory

Intent

Abstract Factory provides a creation interface for a family of related products. A client receives one factory object and obtains each product through its abstract interface, without naming the concrete implementations.

The Problem It Solves

A settings screen needs both a button and a checkbox. Selecting a theme independently for each control can accidentally produce a light button alongside a dark checkbox. Passing one theme factory to the screen keeps that family decision together instead of scattering it across widget construction calls.

Structure

Abstract Factory creates a coherent product family The settings screen receives a GUIFactory. A LightFactory creates a LightButton and LightCheckbox; a DarkFactory creates a DarkButton and DarkCheckbox. Both products are obtained from the same selected factory. Settings screen uses product interfaces GUIFactory LightFactory or DarkFactory Button matching theme Checkbox matching theme 1. inject family 2. create_button() 3. create_checkbox()
Select a family once at the composition boundary, then create each related product through that factory.
One Family Decision, Multiple Products
  1. The application injects a light or dark factory into the settings renderer.
  2. The renderer asks that same factory for a button and a checkbox.
  3. Each product renders through its own interface; the renderer contains no theme-specific branches.

The controls return descriptive strings rather than invoking a GUI toolkit. Matching appearance is the family constraint in this example; other domains may have stronger protocol or platform compatibility requirements.

Python Implementation

from abc import ABC, abstractmethod


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


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


class LightButton(Button):
    def render(self) -> str:
        return "Light button"


class DarkButton(Button):
    def render(self) -> str:
        return "Dark button"


class LightCheckbox(Checkbox):
    def render(self) -> str:
        return "Light checkbox"


class DarkCheckbox(Checkbox):
    def render(self) -> str:
        return "Dark checkbox"


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

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


class LightFactory(GUIFactory):
    def create_button(self) -> Button:
        return LightButton()

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


class DarkFactory(GUIFactory):
    def create_button(self) -> Button:
        return DarkButton()

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


def render_settings(factory: GUIFactory) -> str:
    button = factory.create_button()
    checkbox = factory.create_checkbox()
    return f"{button.render()} | {checkbox.render()}"


if __name__ == "__main__":
    print(render_settings(LightFactory()))
    print(render_settings(DarkFactory()))

Java Implementation

// AbstractFactoryDemo.java

interface Button { String render(); }
interface Checkbox { String render(); }

final class LightButton implements Button {
    public String render() { return "Light button"; }
}

final class DarkButton implements Button {
    public String render() { return "Dark button"; }
}

final class LightCheckbox implements Checkbox {
    public String render() { return "Light checkbox"; }
}

final class DarkCheckbox implements Checkbox {
    public String render() { return "Dark checkbox"; }
}

interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

final class LightFactory implements GUIFactory {
    public Button createButton() { return new LightButton(); }
    public Checkbox createCheckbox() { return new LightCheckbox(); }
}

final class DarkFactory implements GUIFactory {
    public Button createButton() { return new DarkButton(); }
    public Checkbox createCheckbox() { return new DarkCheckbox(); }
}

public class AbstractFactoryDemo {
    static String renderSettings(GUIFactory factory) {
        Button button = factory.createButton();
        Checkbox checkbox = factory.createCheckbox();
        return button.render() + " | " + checkbox.render();
    }

    public static void main(String[] args) {
        System.out.println(renderSettings(new LightFactory()));
        System.out.println(renderSettings(new DarkFactory()));
    }
}

Output

Light button | Light checkbox
Dark button | Dark checkbox

The Extension Trade-off

Adding a new family, such as a high-contrast theme, means adding its products and one new factory. Adding a new product type, such as a slider, changes the factory interface and every concrete factory. Abstract Factory therefore works best when the product categories are reasonably stable but their families vary.

The interface does not magically enforce compatibility. Each concrete factory must actually create a coherent family, and client code must use the selected factory consistently. Replacing the factory affects future creation only; it does not retheme widgets already created. The example needs no factory-of-factories layer.

When to Use It

3. Builder

Intent

Builder separates a multi-step assembly process from the finished object. Named steps collect the necessary parts, while a final build operation creates the product. A reusable construction recipe can be placed in an optional director.

The Problem It Solves

A report grows from a title and a few ordered sections into several named configurations. Large constructor calls obscure what each value means, while scattered mutations can expose unfinished reports. A builder holds the temporary assembly state and publishes a finished report only when the required title is present.

Structure

Builder separates assembly from the finished report The optional director resets and configures a builder. The builder validates its state, creates an immutable report, and resets itself. Future builder changes cannot alter the completed report. ReportDirector orders construction steps TextReportBuilder holds unfinished state Report title + immutable sections 1. configure 2. build() 3. Reset builder state for the next report
The builder is mutable during assembly; the published Report owns a separate, immutable collection.
A Defined Builder Lifecycle
  1. Start from reset state and set the title and ordered sections.
  2. build() rejects an empty title without returning a partial product.
  3. On success, copy the section collection into the report and reset the builder for reuse.

The quarterly recipe and a directly assembled follow-up note use the same builder. Their completed reports remain independent even though the builder is reused.

Python Implementation

from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass(frozen=True)
class Report:
    title: str
    sections: tuple[str, ...]


class ReportBuilder(ABC):
    @abstractmethod
    def reset(self) -> None:
        pass

    @abstractmethod
    def set_title(self, title: str) -> ReportBuilder:
        pass

    @abstractmethod
    def add_section(self, text: str) -> ReportBuilder:
        pass

    @abstractmethod
    def build(self) -> Report:
        pass


class TextReportBuilder(ReportBuilder):
    def __init__(self):
        self.reset()

    def reset(self) -> None:
        self._title = ""
        self._sections: list[str] = []

    def set_title(self, title: str) -> TextReportBuilder:
        self._title = title.strip()
        return self

    def add_section(self, text: str) -> TextReportBuilder:
        self._sections.append(text)
        return self

    def build(self) -> Report:
        if not self._title:
            raise ValueError("A report requires a title")
        result = Report(self._title, tuple(self._sections))
        self.reset()
        return result


class ReportDirector:
    def quarterly(self, builder: ReportBuilder) -> Report:
        builder.reset()
        builder.set_title("Quarterly review")
        builder.add_section("Revenue: 120")
        builder.add_section("Costs: 80")
        return builder.build()


if __name__ == "__main__":
    builder = TextReportBuilder()
    report = ReportDirector().quarterly(builder)
    note = builder.set_title("Follow-up").add_section("Action: review costs").build()

    for product in (report, note):
        print(f"{product.title}: {' | '.join(product.sections)}")
    print(f"First report still has {len(report.sections)} sections")

Java Implementation

// BuilderDemo.java

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

final class Report {
    private final String title;
    private final List<String> sections;

    public Report(String title, List<String> sections) {
        this.title = title;
        this.sections = Collections.unmodifiableList(new ArrayList<>(sections));
    }

    public String getTitle() { return title; }
    public List<String> getSections() { return sections; }
}

interface ReportBuilder {
    void reset();
    ReportBuilder setTitle(String title);
    ReportBuilder addSection(String text);
    Report build();
}

final class TextReportBuilder implements ReportBuilder {
    private String title;
    private final List<String> sections = new ArrayList<>();

    public TextReportBuilder() { reset(); }

    public void reset() {
        title = "";
        sections.clear();
    }

    public TextReportBuilder setTitle(String title) {
        this.title = Objects.requireNonNull(title, "title").trim();
        return this;
    }

    public TextReportBuilder addSection(String text) {
        sections.add(Objects.requireNonNull(text, "section"));
        return this;
    }

    public Report build() {
        if (title.isEmpty()) throw new IllegalStateException("A report requires a title");
        Report result = new Report(title, sections);
        reset();
        return result;
    }
}

final class ReportDirector {
    public Report quarterly(ReportBuilder builder) {
        builder.reset();
        builder.setTitle("Quarterly review");
        builder.addSection("Revenue: 120");
        builder.addSection("Costs: 80");
        return builder.build();
    }
}

public class BuilderDemo {
    public static void main(String[] args) {
        TextReportBuilder builder = new TextReportBuilder();
        Report report = new ReportDirector().quarterly(builder);
        Report note = builder.setTitle("Follow-up").addSection("Action: review costs").build();

        for (Report product : new Report[] {report, note}) {
            System.out.println(product.getTitle() + ": " + String.join(" | ", product.getSections()));
        }
        System.out.println("First report still has " + report.getSections().size() + " sections");
    }
}

Output

Quarterly review: Revenue: 120 | Costs: 80
Follow-up: Action: review costs
First report still has 2 sections

Fluent Calls Are Optional

Returning the builder from its configuration methods enables chaining, but fluent syntax is not what defines the pattern. The important separation is between assembly state and the published product. This example uses one representation to keep the lifecycle visible. A second builder could interpret the same steps as a different representation; when product types differ, the director can perform the steps and let each concrete builder expose its own result.

The Python product uses a tuple of strings; the Java product defensively copies its list and exposes an unmodifiable view. Those choices are sufficient for the immutable elements used here, not a general deep-freezing mechanism. The mutable builder is not thread-safe. Reset-on-success is an explicit policy, not a requirement of Builder; failed builds retain their state so the client can correct it or call reset.

When to Use It

4. Prototype

Intent

Prototype creates a new object by copying a configured instance through a cloning contract. The client requests a duplicate instead of reconstructing that object's concrete class and initialization details.

The Problem It Solves

A document template already contains a title, sections, and tags. Rebuilding its defaults at every call site duplicates configuration logic. Assigning the template to another variable is not a copy, and copying only its outer container leaves nested mutable sections shared. A clone operation gives the document one place to define what a usable independent duplicate means.

Structure

Prototype duplicates a configured object The client requests a clone of an existing Document. The copy operation creates a new Document, new Section objects, and new tag lists. Editing the clone does not change the original template. Template configured Document clone() / copy() copy policy lives here New Document independent mutable state 1. request clone 2. copy sections 3. Add a regional tag to the clone; the template stays unchanged
New identity does not imply independent internals: the clone operation must define which nested objects to copy.
Clone, Then Customize
  1. Create a template containing a summary section and a baseline tag.
  2. Request a clone that copies the mutable section objects and tag lists.
  3. Rename the clone and append a regional tag; verify that the original still has only its baseline tag.

For this small object tree, Python delegates to deepcopy(); Java uses explicit copy constructors rather than Object.clone() or Cloneable.

Python Implementation

from __future__ import annotations
from abc import ABC, abstractmethod
from copy import deepcopy
from dataclasses import dataclass, field


class Prototype(ABC):
    @abstractmethod
    def clone(self) -> Prototype:
        pass


@dataclass
class Section:
    text: str
    tags: list[str] = field(default_factory=list)


@dataclass
class Document(Prototype):
    title: str
    sections: list[Section] = field(default_factory=list)

    def clone(self) -> Document:
        return deepcopy(self)


if __name__ == "__main__":
    template = Document("Quarterly review", [Section("Summary", ["baseline"])])
    regional = template.clone()
    regional.title = "Regional review"
    regional.sections[0].tags.append("regional")

    print(f"Original: {template.title} / {', '.join(template.sections[0].tags)}")
    print(f"Clone: {regional.title} / {', '.join(regional.sections[0].tags)}")
    independent = regional.sections[0] is not template.sections[0]
    print(f"Nested sections independent: {'yes' if independent else 'no'}")

Java Implementation

// PrototypeDemo.java

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

interface Prototype<T> { T copy(); }

final class Section {
    private final String text;
    private final List<String> tags;

    public Section(String text, List<String> tags) {
        this.text = text;
        this.tags = new ArrayList<>(tags);
    }

    public Section(Section source) {
        this(source.text, source.tags);
    }

    // Tags are deliberately editable in this example.
    public List<String> getTags() { return tags; }
}

final class Document implements Prototype<Document> {
    private String title;
    private final List<Section> sections;

    public Document(String title, List<Section> sections) {
        this.title = title;
        this.sections = new ArrayList<>(sections);
    }

    private Document(Document source) {
        title = source.title;
        sections = new ArrayList<>();
        for (Section section : source.sections) {
            sections.add(new Section(section));
        }
    }

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public List<Section> getSections() { return sections; }

    @Override
    public Document copy() { return new Document(this); }
}

public class PrototypeDemo {
    public static void main(String[] args) {
        Document template = new Document("Quarterly review", Arrays.asList(
                new Section("Summary", Arrays.asList("baseline"))));
        Document regional = template.copy();
        regional.setTitle("Regional review");
        regional.getSections().get(0).getTags().add("regional");

        System.out.println("Original: " + template.getTitle() + " / "
                + String.join(", ", template.getSections().get(0).getTags()));
        System.out.println("Clone: " + regional.getTitle() + " / "
                + String.join(", ", regional.getSections().get(0).getTags()));
        boolean independent = regional.getSections().get(0) != template.getSections().get(0);
        System.out.println("Nested sections independent: " + (independent ? "yes" : "no"));
    }
}

Output

Original: Quarterly review / baseline
Clone: Regional review / baseline, regional
Nested sections independent: yes

Shallow Copy vs. Deep Copy

A shallow copy creates a new outer object but retains references to its nested objects. A deep copy recursively copies nested state. Python's copy module uses a memo during deep copying to handle cycles and preserve repeated references within the copied graph. The Java demo handles an acyclic tree only: it copies each section occurrence independently, so preserving aliases or cycles would require an explicit identity map.

Define the copy boundary deliberately. Immutable values can often be shared; open files, sockets, locks, and database sessions require domain-specific handling rather than indiscriminate cloning. Copying is not inherently faster than constructing a new object. For large graphs, measure the cost and consider custom copy logic.

A registry of named prototypes is optional. If one is added, return clones rather than exposing a mutable shared template, and define whether registration also copies its input.

When to Use It

5. Singleton

Intent

Singleton combines controlled creation of one instance with a shared access point. Its distinguishing question is not which product to create, but how callers obtain the same instance within a defined runtime scope.

The Problem It Solves

Several worker threads need the same runtime configuration. An unprotected check-then-create sequence can allocate multiple instances under concurrent first access. A Python implementation that only protects __new__() can also run __init__() repeatedly on the reused object. Construction and publication must therefore be treated as one lifecycle operation.

Structure

Singleton funnels callers through one creation boundary Concurrent callers use one access boundary. A Python metaclass lock protects construction and initialization, while Java uses class initialization of a nested holder. Later calls return the same RuntimeConfig within the documented runtime scope. Caller A Caller B Access boundary synchronized creation RuntimeConfig same published instance 1. Check cached instance 2. Create once or reuse Python: metaclass lock. Java: initialization-on-demand holder.
Publication must finish initialization before another caller can receive the instance.
Initialize Before Publishing
  1. Sixteen tasks request the configuration through the controlled access path.
  2. The first successful construction initializes the fixed values before publishing the reference.
  3. Later requests reuse that reference; the demo checks that every returned object is identical.

Python takes the lock on every call, favoring clarity over a double-checked optimization. Java relies on the JVM's class-initialization guarantees for its lazy holder. No external configuration file or service is required.

Python Implementation

from concurrent.futures import ThreadPoolExecutor
from threading import Lock
from types import MappingProxyType


class SingletonMeta(type):
    _instances: dict[type, object] = {}
    _lock = Lock()

    def __call__(cls):
        # Lock covers construction AND initialization, not just __new__.
        with cls._lock:
            if cls not in cls._instances:
                instance = super().__call__()
                cls._instances[cls] = instance
            return cls._instances[cls]


class RuntimeConfig(metaclass=SingletonMeta):
    def __init__(self):
        self._values = MappingProxyType({
            "environment": "production",
            "timeout": 30,
        })

    def get(self, key: str) -> str | int:
        return self._values[key]


if __name__ == "__main__":
    with ThreadPoolExecutor(max_workers=8) as pool:
        configs = list(pool.map(lambda _: RuntimeConfig(), range(16)))

    first = configs[0]
    same = all(config is first for config in configs)
    print(f"One instance across threads: {'yes' if same else 'no'}")
    print(f"Environment: {first.get('environment')}")
    print(f"Timeout: {first.get('timeout')}")

Java Implementation

// SingletonDemo.java

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

final class RuntimeConfig {
    private final Map<String, String> values;

    private RuntimeConfig() {
        Map<String, String> initial = new HashMap<>();
        initial.put("environment", "production");
        initial.put("timeout", "30");
        values = Collections.unmodifiableMap(initial);
    }

    // JVM class initialization safely publishes the instance.
    private static class Holder {
        private static final RuntimeConfig INSTANCE = new RuntimeConfig();
    }

    public static RuntimeConfig getInstance() { return Holder.INSTANCE; }
    public String get(String key) { return values.get(key); }
}

public class SingletonDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(8);
        try {
            List<Future<RuntimeConfig>> tasks = new ArrayList<>();
            for (int i = 0; i < 16; i++) {
                tasks.add(pool.submit(() -> RuntimeConfig.getInstance()));
            }
            RuntimeConfig first = tasks.get(0).get();
            boolean same = true;
            for (Future<RuntimeConfig> task : tasks) {
                same &= task.get() == first;
            }
            System.out.println("One instance across threads: " + (same ? "yes" : "no"));
            System.out.println("Environment: " + first.get("environment"));
            System.out.println("Timeout: " + first.get("timeout"));
        } finally {
            pool.shutdown();
        }
    }
}

Output

One instance across threads: yes
Environment: production
Timeout: 30

Scope, Concurrency, and Initialization Policy

The Python cache is tied to a class object in its interpreter; Java's static instance is tied to its defining class loader. Neither coordinates one instance across processes or machines. These implementations cover ordinary calls through the documented access paths, not deliberate reflection, custom deserialization, or low-level construction bypasses.

The Python metaclass accepts no configuration arguments, avoiding an ambiguous first-call-wins parameter policy. It stores the instance only after successful initialization. Its shared, non-reentrant lock assumes constructors do not instantiate another class using the same metaclass. This small example has no such dependency; a more general service lifecycle needs a different design.

Thread-safe initialization does not make arbitrary instance methods thread-safe. The demo exposes read-only mappings of immutable values and performs no later configuration writes. Java keeps values as strings; Python retains the integer timeout. The identical printed output does not imply identical value types.

Global access can hide dependencies, complicate tests, and couple state across callers. When only one application-owned instance is needed, construct it at startup and inject it into consumers instead. A module-level object can also be a simpler Python solution when class-level enforcement is unnecessary. A database connection pool is not the same thing as one globally shared connection.

When to Use It

6. Detailed Comparison

Pattern Selection Guide

Start with the source of change rather than the pattern name. The following guide makes the design boundary and its required contract explicit.

Design PressurePatternVariation BoundaryWhat Must Be DefinedAvoid It When
One collaborator varies inside an inherited workflowFactory MethodCreator subclass supplies a productProduct ownership and the creation hookA direct constructor or injected callable is sufficient
Multiple product types must vary as one familyAbstract FactoryInjected factory selects matching implementationsWhich products belong to a familyProducts are unrelated or product categories change constantly
Assembly has meaningful steps or reusable recipesBuilderBuilder state and construction sequenceValidation, result ownership, and reset behaviorKeyword arguments already express construction clearly
New instances start from configured runtime objectsPrototypeClone operation of the source objectCopy depth, identities, aliases, and resource handlingFresh construction is clearer or the state cannot safely be copied
Callers require one shared identity and access pointSingletonControlled creation and publicationScope, concurrency, initialization, and test isolationOne injected application-owned instance meets the requirement
Resolve the Closest Choices

Common Pitfalls

PatternCommon PitfallBetter Approach
Factory MethodCalling every factory helper the GoF Factory Method patternIdentify the overridden creation hook and the workflow that uses it
Abstract FactoryAllowing callers to mix independently chosen familiesInject one coherent factory at the composition boundary
BuilderPublishing mutable assembly collections directlyGive the completed product independent storage and document reset policy
PrototypeAssuming a new outer object implies independent nested stateTest nested mutations and document the copy policy
SingletonTreating a global instance as automatically thread-safe or system-wideSpecify runtime scope and separate publication safety from mutable-state safety

7. Putting It All Together

A useful creational abstraction hides a decision that actually needs to vary. Keep ordinary constructors when no such pressure exists; add a pattern when it creates a clearer extension or lifecycle boundary.

Construction Contracts Worth Making Explicit

Keep the application boundary responsible for selecting concrete creators and factories. Let consumers work with the resulting contracts. That keeps configuration choices visible while allowing product implementations and construction details to evolve independently.

Pattern terminology and relationships can be explored in the Creational Patterns catalog; the examples here focus on explicit creation, ownership, and runtime behavior.