Loading learning content...
Knowing how patterns compare is necessary but not sufficient. When you're in the middle of designing a system, you need a systematic process for arriving at the right pattern—not just a mental catalog of options.
This page presents a decision tree: a flowchart of questions that systematically leads you from your problem's characteristics to the appropriate creational pattern. Think of it as a diagnostic tool—each question narrows the space of candidates until you arrive at the best fit.
The decision tree approach has a crucial advantage: it externalizes the decision process, making it reviewable, teachable, and consistent. Instead of relying on intuition (which can be biased or incomplete), you follow a principled path.
By the end of this page, you will have a systematic decision tree for selecting creational patterns. You'll learn the key discriminating questions, understand why each question matters, and be able to apply this framework to real design decisions. You'll move from ad-hoc pattern selection to principled decision-making.
Before any pattern selection can occur, you must articulate the specific problem you're trying to solve with object creation. This isn't just "I need to create objects"—that's true for all software. You need to identify the tension or pain point in your current approach.
Creational patterns address specific tensions:
If you can't clearly articulate the creational tension you're facing, stop and think harder. Applying patterns without a clear problem leads to over-engineering. The pattern should solve a genuine pain—if there's no pain, there's no need for a pattern.
The diagnostic framing:
Try completing these sentences to clarify your problem:
Once you've framed the problem, you're ready to enter the decision tree.
The decision tree starts with high-level categorizing questions and progressively narrows to specific patterns. Here's the complete tree, followed by detailed explanations of each branch.
Level 1: What is the primary concern?
The first question separates patterns into three families:
This tree visualizes the decision flow. Let's explore each path in detail.
You've identified that your problem is coupling—client code is too tightly bound to the concrete classes it creates. This is the most common creational concern and leads to Factory-family patterns.
Key sub-question: Do you need families of compatible objects?
This question distinguishes between:
Sub-question for single products: Does subclass determine the product type?
If you're designing a framework where subclasses customize instantiation, use Factory Method. The framework defines when to create; subclasses define what to create.
If you simply want to encapsulate creation logic without an inheritance hierarchy, a Simple Factory (just a function or class that creates objects) is sufficient. This isn't a GoF pattern, but it's often the right pragmatic choice.
Decision examples:
| Scenario | Pattern Choice | Reasoning |
|---|---|---|
| Building a plugin system where plugins define their UI components | Factory Method | Plugin subclass determines what component to create |
| Creating Windows/Mac/Linux UI that's consistent per platform | Abstract Factory | Multiple products (button, menu, dialog) must match |
| Simple service layer creating DTOs from domain objects | Simple Factory | No inheritance needed, just encapsulate creation |
| ORM framework that creates DB-specific connections and commands | Abstract Factory | Connection and command must be compatible (same DB) |
| Game engine where each level subclass creates its own enemy types | Factory Method | Level subclass determines enemy type |
Many situations that seem to need Factory Method are better served by simple factory functions. Reserve the formal Factory Method pattern for genuine framework-level customization hooks where inheritance is the right abstraction.
You've identified that your problem involves instance management—not what is created but how many instances exist and how they're managed. This branch leads to Singleton, Prototype, and Object Pool.
Key sub-question: Do you need exactly one instance?
This is the first filter in the lifecycle branch. If the answer is yes, Singleton is the candidate (though proceed with caution—Singleton is often overused).
If not Singleton: Do you want to reuse existing objects?
If you're creating new instances each time but want to leverage existing configuration or avoid expensive initialization, you're choosing between Prototype and Object Pool.
Sub-question: Do you need borrow/return semantics?
This distinguishes the two patterns:
| Scenario | Pattern Choice | Reasoning |
|---|---|---|
| System-wide logging configuration | Singleton (with caution) or DI-scoped | One configuration affects all logging |
| Game spawning enemies from pre-configured archetypes | Prototype | Clone enemy templates; each instance independent |
| HTTP client library managing connections | Object Pool | Connections expensive; reuse across requests |
| Image editor with template documents | Prototype | Clone templates; users modify their own copy |
| Thread allocations in high-throughput system | Object Pool | Thread creation expensive; bounded pool appropriate |
With Prototype, you get a new copy each time—you own it, modify it, discard it. With Object Pool, you borrow temporarily—you must return it, and you shouldn't assume its state persists. If you need permanent ownership of a configured object, use Prototype. If you need temporary access to an expensive resource, use Object Pool.
You've identified that your problem is construction complexity—the object's production is complicated, not its use. This branch leads directly to Builder.
Builder is appropriate when construction involves:
Diagnostic questions for Builder:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
// Scenario: When does this justify Builder? // BEFORE: Telescoping constructors (anti-pattern)class HttpRequest { constructor(url: string); constructor(url: string, method: string); constructor(url: string, method: string, headers: Map<string, string>); constructor(url: string, method: string, headers: Map<string, string>, body: string); constructor(url: string, method: string, headers: Map<string, string>, body: string, timeout: number); constructor(url: string, method: string, headers: Map<string, string>, body: string, timeout: number, retries: number); // ... This is painful. Builder is clearly warranted.} // AFTER: Builder patternclass HttpRequestBuilder { private url: string; private method: string = "GET"; private headers: Map<string, string> = new Map(); private body?: string; private timeout: number = 30000; private retries: number = 0; constructor(url: string) { this.url = url; } setMethod(method: string): this { this.method = method; return this; } addHeader(key: string, value: string): this { this.headers.set(key, value); return this; } setBody(body: string): this { this.body = body; return this; } setTimeout(timeout: number): this { this.timeout = timeout; return this; } setRetries(retries: number): this { this.retries = retries; return this; } build(): HttpRequest { // Validate and construct return new HttpRequest( this.url, this.method, this.headers, this.body, this.timeout, this.retries ); }} // Usage is clear and self-documenting:const request = new HttpRequestBuilder("https://api.example.com") .setMethod("POST") .addHeader("Content-Type", "application/json") .setBody('{"key": "value"}') .setTimeout(5000) .build();For even more complex scenarios, add a Director that encapsulates specific construction algorithms. The Director uses the Builder interface to construct objects, allowing the same Director to work with different Builders and produce different representations.
When NOT to use Builder:
Builder adds classes and indirection. Avoid it when:
A simple constructor or factory function is often sufficient. Don't introduce Builder for a class with 3 required parameters and no complexity.
Let's walk through the decision tree with complete examples to demonstrate the systematic process.
Scenario: E-commerce platform needs to support multiple payment gateways (Stripe, PayPal, Square). Payment processing code shouldn't know which gateway is active.
Decision tree walk-through:
Conclusion: Simple Factory. A factory function or class that takes a configuration and returns the appropriate processor. No need for full Factory Method inheritance.
function createPaymentProcessor(config: PaymentConfig): PaymentProcessor {
switch (config.provider) {
case 'stripe': return new StripeProcessor(config.apiKey);
case 'paypal': return new PayPalProcessor(config.clientId);
case 'square': return new SquareProcessor(config.accessToken);
default: throw new Error(`Unknown provider: ${config.provider}`);
}
}
Even with a systematic decision tree, engineers fall into common traps. Awareness of these pitfalls helps you avoid them.
Use the simplest pattern that solves your problem. If a factory function works, don't use Factory Method. If a constructor is clear, don't use Builder. Patterns are tools for solving problems, not decorations for code.
Validation questions before finalizing:
Before committing to a pattern, ask yourself:
If you can't clearly answer these questions, reconsider your choice.
We've built a systematic decision framework for selecting creational patterns. Let's consolidate the key elements:
What's next:
Patterns rarely exist in isolation. The next page explores combining patterns—how creational patterns work together and with other pattern categories to create sophisticated, flexible designs.
You now have a systematic decision tree for selecting creational patterns. You understand the key discriminating questions, can apply the framework to real scenarios, and know the common pitfalls to avoid. Next, we'll explore how patterns combine.