./articles/ Software Engineering Essentials: Creational Design Patterns
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
| Pattern | Core Intent | Key Benefit |
|---|---|---|
| Factory Method | Delegate a creation hook to subclasses | Extend a shared workflow with a different product |
| Abstract Factory | Create a family of related products | Keep the family selection consistent across product types |
| Builder | Separate stepwise assembly from the finished object | Name construction steps and control publication |
| Prototype | Copy a configured object through a cloning contract | Reuse runtime presets without reconstructing them in client code |
| Singleton | Control one instance and a shared access point | Make 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
- Product:
Transportdefinesdeliver(parcel). - Concrete Products:
TruckandShipprovide different delivery behavior. - Creator:
Logisticsownsplan_delivery()and declares the factory methodcreate_transport(). - Concrete Creators:
RoadLogisticsandSeaLogisticsoverride only the creation hook.
From Product Selection to Delivery
- The application selects a concrete logistics service at its configuration boundary.
- The inherited planning method calls the creation hook on that service.
- 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 seaFactory 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
- When a framework or reusable workflow should let subclasses choose one collaborating product.
- When adding a product variant should not require rewriting the inherited operation that uses 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 Products:
ButtonandCheckboxare separate product types. - Concrete Products: the light and dark implementations form two matching families.
- Abstract Factory:
GUIFactorydeclares a creation method for each product type. - Concrete Factories:
LightFactoryandDarkFactoryreturn the matching variants. - Client:
render_settings()depends only on the factory and product contracts.
One Family Decision, Multiple Products
- The application injects a light or dark factory into the settings renderer.
- The renderer asks that same factory for a button and a checkbox.
- 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 checkboxThe 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
- When several product types must vary together as one platform, provider, or theme family.
- When clients should be reusable across families without importing their concrete product classes.
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
- Product:
Reportstores a title and an immutable sequence of section strings. - Builder:
ReportBuilderdefines reset, title, section, and build operations. - Concrete Builder:
TextReportBuildercollects data and produces the report. - Director:
ReportDirectorencapsulates the quarterly recipe, but clients may also use the builder directly.
A Defined Builder Lifecycle
- Start from reset state and set the title and ordered sections.
build()rejects an empty title without returning a partial product.- 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 sectionsFluent 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
- When meaningful assembly steps, validation, or reusable recipes make a large constructor difficult to read.
- When construction must hide incomplete state. For a small Python object, keyword arguments or a dataclass may already be enough.
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: a contract for
clone()in Python orcopy()in the Java example. - Concrete Prototype:
Documentowns the copy policy for its sections and their tag lists. - Client: clones a prepared template and then customizes the duplicate.
Clone, Then Customize
- Create a template containing a summary section and a baseline tag.
- Request a clone that copies the mutable section objects and tag lists.
- 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: yesShallow 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
- When new objects should start from rich, runtime-configured presets instead of repeated initialization recipes.
- When a client knows a cloning interface but should not know each concrete class's constructor.
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
- Shared Product:
RuntimeConfigexposes fixed environment and timeout values. - Python Creation Boundary:
SingletonMetacaches one successfully initialized instance per class and serializes creation with a lock. - Java Creation Boundary: a private constructor and nested holder publish the instance through
getInstance(). - Clients: concurrent tasks request the configuration and compare object identity.
Initialize Before Publishing
- Sixteen tasks request the configuration through the controlled access path.
- The first successful construction initializes the fixed values before publishing the reference.
- 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: 30Scope, 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
- When a deliberately shared identity and global access are both part of the contract, and the runtime scope is explicit.
- Only after checking that explicit ownership and dependency injection would not satisfy the requirement more simply.
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 Pressure | Pattern | Variation Boundary | What Must Be Defined | Avoid It When |
|---|---|---|---|---|
| One collaborator varies inside an inherited workflow | Factory Method | Creator subclass supplies a product | Product ownership and the creation hook | A direct constructor or injected callable is sufficient |
| Multiple product types must vary as one family | Abstract Factory | Injected factory selects matching implementations | Which products belong to a family | Products are unrelated or product categories change constantly |
| Assembly has meaningful steps or reusable recipes | Builder | Builder state and construction sequence | Validation, result ownership, and reset behavior | Keyword arguments already express construction clearly |
| New instances start from configured runtime objects | Prototype | Clone operation of the source object | Copy depth, identities, aliases, and resource handling | Fresh construction is clearer or the state cannot safely be copied |
| Callers require one shared identity and access point | Singleton | Controlled creation and publication | Scope, concurrency, initialization, and test isolation | One injected application-owned instance meets the requirement |
Resolve the Closest Choices
- Factory Method or Abstract Factory: ask whether a subclass must customize one workflow collaborator, or a client must receive several matching product types. An Abstract Factory may implement its creation operations using factory methods; the intents remain different.
- Abstract Factory or Builder: ask whether the decision is which coherent family to create, or how to assemble a product over a sequence of steps.
- Builder or Prototype: use a construction recipe when steps define the result; use a copy contract when a configured instance is the starting point.
- Singleton or explicit ownership: sharing one instance does not by itself require enforcing a global creation rule.
Common Pitfalls
| Pattern | Common Pitfall | Better Approach |
|---|---|---|
| Factory Method | Calling every factory helper the GoF Factory Method pattern | Identify the overridden creation hook and the workflow that uses it |
| Abstract Factory | Allowing callers to mix independently chosen families | Inject one coherent factory at the composition boundary |
| Builder | Publishing mutable assembly collections directly | Give the completed product independent storage and document reset policy |
| Prototype | Assuming a new outer object implies independent nested state | Test nested mutations and document the copy policy |
| Singleton | Treating a global instance as automatically thread-safe or system-wide | Specify 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
- Ownership: does a call return a fresh product, a copy, a pooled object, or a shared instance? Clients should not have to infer this from a method name.
- Validity: when does validation occur, and can a partially built or initialized object escape to another caller?
- Isolation: can later builder edits, prototype mutations, or concurrent access change a product that a client already holds?
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.