Creational Patterns: Control Object Creation
Compare five creational patterns, see a compact TypeScript example, and choose the simplest fit for product families, construction, copying, or shared state.
Creational patterns move object construction decisions behind a boundary when callers repeat rules or depend on concrete classes. This guide compares Abstract Factory, Builder, Factory Method, Prototype, and Singleton, with a TypeScript example of a notification workflow. Use the trade-off table and failure scenarios to choose the smallest construction seam that fits, or keep direct construction when it is clearer.
Creational Patterns: Control Object Creation
Introduction
Object creation gets complicated when callers must know concrete classes, coordinate several related objects, or repeat construction rules. The five classic Gang of Four creational patterns move some of that work behind a boundary. They do not remove complexity; they put it somewhere that can own it.
This guide compares Abstract Factory, Builder, Factory Method, Prototype, and Singleton. The useful question is not “Which pattern should I use?” Start with the pressure you can point to in the code: a family of compatible products, a long construction process, a varying product type, an expensive copy, or shared state with a deliberate lifetime. If none applies, new is probably enough.
The five patterns at a glance
Abstract Factory: select a compatible family
Abstract Factory gives a client an interface for creating a set of related products. A desktop theme could provide a button and a dialog from the same visual family. A test environment might provide a fake repository and fake message bus that are meant to be used together.
Use it when the family is the unit of variation. The client asks for products through a factory contract and does not mix implementations from different families. The cost is another interface and one factory implementation per family. Adding a new product kind can also require changing every factory, so this pattern works best when the product set is fairly stable.
Builder: assemble an object in explicit steps
Builder separates a multi-step construction process from the final representation. It is useful when an object has many optional settings, validation rules between steps, or multiple construction paths that should end in a valid result. An HTTP request builder can make headers, query parameters, and a body readable without a constructor containing a dozen positional arguments.
Builder is often unnecessary for a small value object with two required fields. A named options object can solve that problem with less machinery. If each step merely assigns a field and there is no invariant to protect, ask whether the builder has earned its extra type and methods.
Factory Method: let a subtype or injected hook choose a product
Factory Method defines a creation point that subclasses or a supplied function can vary. A document importer might keep the import workflow stable while each importer creates its matching parser. In functional or dependency-injected code, a factory function passed at the boundary often gives the same practical seam without an inheritance hierarchy.
The important part is where the decision belongs. If callers repeatedly branch on a format and construct the corresponding parser, centralize that choice. If there is only one parser and no expected variation, direct construction is easier to follow.
Prototype: make a new object from a configured exemplar
Prototype creates an object by copying a configured instance. It can help when the initial state is expensive or awkward to reconstruct, or when a runtime-defined template needs to produce similar objects. Cloning must have clear semantics: should nested references be shared, copied recursively, or reset?
In languages with mutable object graphs, a shallow copy can look correct while allowing one clone to mutate another. Prefer an explicit clone operation that documents the fields and ownership rules. If constructing a fresh object is cheap, a constructor or factory is usually clearer.
Singleton: constrain a type to one instance
Singleton restricts construction and exposes a shared instance. It can be appropriate for a process-wide immutable registry or a resource whose lifecycle truly belongs to the process. It becomes troublesome when unrelated code reaches global mutable state through a static accessor: tests leak state, dependencies become invisible, and multiple application contexts cannot be isolated.
Many dependency injection containers can provide one instance per application scope without making the class globally accessible. That keeps lifetime configuration at the composition boundary. A “one instance for the whole process” requirement should be explicit; one instance per request, tenant, worker, or test is often the actual need.
A compact Factory Method example
Imagine a notification workflow that should support email and SMS. The workflow does not need to know the vendor client classes. It can ask a factory for a sender and then use a small interface:
interface Notification {
recipient: string;
message: string;
}
interface Sender {
send(notification: Notification): Promise<void>;
}
type Delivery = (notification: Notification) => Promise<void>;
class EmailSender implements Sender {
constructor(private readonly deliver: Delivery) {}
send(notification: Notification): Promise<void> {
return this.deliver(notification);
}
}
class SmsSender implements Sender {
constructor(private readonly deliver: Delivery) {}
send(notification: Notification): Promise<void> {
return this.deliver(notification);
}
}
abstract class NotificationService {
constructor(protected readonly deliver: Delivery) {}
protected abstract createSender(): Sender;
send(notification: Notification): Promise<void> {
if (!notification.recipient.trim() || !notification.message.trim()) {
throw new Error("Recipient and message are required");
}
return this.createSender().send(notification);
}
}
class EmailNotificationService extends NotificationService {
protected createSender(): Sender {
return new EmailSender(this.deliver);
}
}
class SmsNotificationService extends NotificationService {
protected createSender(): Sender {
return new SmsSender(this.deliver);
}
}
The base workflow validates the request and calls createSender(). Each concrete service overrides that method and supplies the matching sender, while the provider-specific delivery function arrives through construction. Tests can provide a fake delivery function or sender. If a system has only one channel, the subclass hierarchy is extra work; a single Sender injected into the workflow is simpler. If channel selection must happen per message, inject a factory function instead. That change in where variation occurs is the design decision; calling a helper “Factory Method” does not make it useful by itself.
flowchart LR
Config[Application configuration] --> Factory[Sender factory]
Factory -->|creates| Email[Email sender]
Factory -->|creates| SMS[SMS sender]
Email --> Contract[Sender contract]
SMS --> Contract
Contract --> Service[Notification service]
Service --> Message[Notification]
The diagram follows the dependency direction in the example: the workflow relies on the sender contract, while application wiring supplies one concrete implementation. The factory contains the choice of concrete type.
When to Use
Use a creational pattern when a concrete construction problem keeps leaking into callers or causes invalid combinations. Look for evidence such as repeated type switches, duplicated validation, inconsistent optional defaults, or callers assembling products from different families.
- Choose Abstract Factory when callers need a matched set of products and the family varies as a unit.
- Choose Builder when construction has meaningful stages or invariants and positional arguments obscure intent.
- Choose Factory Method when a stable workflow needs one product choice to vary behind a boundary.
- Choose Prototype when copying a configured exemplar is clearer or cheaper than rebuilding it, with copy semantics made explicit.
- Choose Singleton only when a single instance is a real lifecycle constraint and global access is acceptable.
If the problem is simply passing dependencies into an object, a constructor and a composition root may be all you need. See dependency injection and application state for object wiring and lifetime choices.
When NOT to Use
Do not add a pattern because a design-pattern catalog lists it or because every class seems to need an interface. Keep direct construction when there is one stable product, short construction logic, and a small number of callers. A factory that wraps one new expression without centralizing a decision has added a hop, not a useful seam.
Avoid Abstract Factory when products do not need to stay compatible, Builder when an options object is enough, and Prototype when copying would hide ownership rules. Avoid Singleton as a shortcut for dependency injection or caching. A global singleton does not make concurrency, lifecycle, or invalidation concerns disappear.
Production Failure Scenarios
- Mixed product families: Separate factories or fallback paths create a button from one UI theme and a dialog from another. Keep one family selection at the composition boundary and test the combinations clients can receive.
- Incomplete builders: A builder returns an object before a required field is set, or validates one field but not a cross-field rule. Make
build()enforce invariants and return a valid type only after validation. - Factory drift: A new channel is added to configuration but omitted from a factory switch. Make the type system force exhaustive handling and alert on unknown configuration rather than silently choosing a default.
- Prototype aliasing: A cloned object shares a mutable nested list with its prototype. Specify whether each field is copied, reused, or reset, then test mutation isolation where callers expect it.
- Singleton state leakage: A test or tenant changes global state that another request reads. Keep mutable state scoped to the owner and reset or replace it through explicit lifecycle wiring.
- Construction overload: A factory becomes a hidden service locator and begins loading configuration, connecting to external systems, and making policy decisions. Keep product creation local; let the application composition layer own lifecycle and external connections.
Trade-Off Table
| Pattern | Construction pressure | Main benefit | Main cost | Simpler option to check first |
|---|---|---|---|---|
| Abstract Factory | Several products must come from one compatible family | Client code stays independent of concrete families | More factory types; adding product kinds touches each family | Pass the one required implementation directly |
| Builder | Many construction steps, options, or invariants | Names steps and guards a valid final object | More methods and a second construction API | Named options object or static constructor |
| Factory Method | Product selection varies while workflow stays stable | Moves concrete selection behind a seam | Indirection and possible subclass hierarchy | Constructor parameter or a small function |
| Prototype | A configured exemplar is costly or useful to copy | Reuses setup through a defined clone operation | Copy depth and shared ownership can be subtle | Fresh constructor or factory call |
| Singleton | Exactly one instance has a defined owner and lifetime | Centralizes one shared process resource | Hidden global dependency and hard-to-isolate state | Scoped dependency injection |
Observability Checklist
Creational code rarely needs its own metrics by default. Instrument the behavior around it when construction affects reliability or cost:
- Record the selected product family or channel when it helps diagnose configuration errors; avoid logging secrets or full user payloads.
- Track factory creation failures, builder validation failures, and prototype clone failures with bounded error categories.
- Measure initialization latency for expensive resources, and distinguish startup work from per-request creation.
- Expose the configured lifetime of shared instances in diagnostics so operators can tell whether a resource is process-, tenant-, or request-scoped.
- Add traces around real external initialization, not every trivial object allocation.
Security and Compliance Notes
Factories often sit where untrusted configuration selects an implementation. Map inputs through an allowlist such as a discriminated union; never turn arbitrary user-provided class names into dynamic imports or reflective construction. Validate configuration before creating clients, and keep credentials in the appropriate secret provider rather than in prototypes, default builders, or logs.
Treat clones as potential data copies. A prototype may carry tenant identifiers, access tokens, personal data, or stale authorization decisions into a new object. Reset security-sensitive fields or create them from trusted context. For Singleton resources, verify that shared caches and mutable state cannot cross tenant boundaries. The same lifecycle questions apply to privacy retention and audit requirements: a global object can keep data alive longer than intended.
Common Pitfalls / Anti-Patterns
- Factory for every class: A one-line wrapper can make navigation harder without removing any real caller knowledge.
- Abstract Factory as a registry: A factory that returns unrelated services is a service locator with a different name.
- Telescoping builder: A long fluent API duplicates constructor fields but provides no validation or meaningful sequence.
- Inheritance-only Factory Method: A subclass hierarchy may be heavier than an injected function when only one creation decision varies.
- Unspecified clone depth: Shallow and deep copy are not interchangeable. Document the contract in code and tests.
- Singleton by default: A static
getInstance()can make dependencies invisible and tests order-dependent. - Creation mixed with lifecycle: Selecting a type, opening network connections, retrying, and shutting down are separate concerns. Keep ownership clear.
For the broader design context around responsibilities and dependencies, read SOLID Principles in Practice and the Object-Oriented Design & Design Patterns Roadmap.
Quick Recap Checklist
- Name the specific creation problem before selecting a pattern.
- Keep the choice close to the composition boundary unless product selection must happen later.
- Enforce object invariants at the point where a valid object is produced.
- Define whether clones share or copy nested state.
- Give shared instances an explicit owner and lifetime.
- Prefer a constructor, function, or options object when it solves the problem with fewer concepts.
Interview Questions
Abstract Factory creates a compatible family of related products through a set of creation operations. Factory Method provides a point where one product choice can vary, often through an overridable method or injected function. Use the family boundary when products must match; use the single creation seam when one product varies.
Builder helps when construction has meaningful stages, conditional steps, or cross-field invariants that should be checked before returning the object. For a handful of independent optional values, a typed options object is usually shorter and easier to maintain.
A globally reachable instance hides a dependency and can preserve mutable state between tests or requests. In a multi-tenant system, the same state may cross tenant boundaries. An explicitly scoped dependency keeps the lifetime visible and lets tests supply isolated instances.
Further Reading
- Refactoring.Guru: Creational Design Patterns — overview and examples for the five classic patterns.
- Refactoring.Guru: Factory Method — a focused explanation of the creation seam and its trade-offs.
- SOLID Principles in Practice — design heuristics that help decide where an abstraction belongs.
- Dependency Injection and Application State — wiring dependencies and choosing their lifetimes.
Conclusion
Creational patterns are useful when they put a real construction rule behind the right boundary. Abstract Factory coordinates product families, Builder manages staged construction, Factory Method moves a product choice, Prototype defines copying, and Singleton constrains instance lifetime. Their abstractions have costs, so start with the pressure in the code and choose the smallest mechanism that makes that pressure easier to manage.
Category
Related Posts
Refactoring Toward Patterns Safely
Refactor toward design patterns safely: follow evidence of change, preserve behavior with tests, and make small transformations before adding an abstraction.
SOLID Principles in Practice
Apply all five SOLID principles to a small checkout design, see where they help, and learn when extra interfaces or indirection make code harder to change.
Behavioral Patterns: Organize Collaboration
Compare all eleven GoF behavioral patterns by the change they isolate, the coupling they reduce, and the runtime costs they add to a system in real code.