Loading content...
Every object-oriented system begins with a deceptively simple act: creating objects. A single line of code—new Customer() or new PaymentProcessor()—seems straightforward. Yet as systems grow in scale and complexity, this simplicity becomes a source of profound architectural challenges.
Consider what happens when you need to:
These scenarios reveal that object creation is not merely a mechanical step—it's a design decision with far-reaching consequences for flexibility, testability, and maintainability.
This page explores Creational Design Patterns—the first category in the Gang of Four's taxonomy. You'll understand why object creation deserves dedicated design attention, the problems creational patterns solve, and how each pattern in this category approaches instantiation differently. By the end, you'll recognize when object creation has become a design problem requiring a creational pattern solution.
In introductory programming, object creation is taught as a syntactic necessity—you need objects, so you instantiate them. But at the architectural level, how you create objects shapes your entire system's design.
The fundamental tension:
Object-oriented design encourages programming to abstractions (interfaces, abstract classes) rather than concrete implementations. This principle—central to the Dependency Inversion Principle and the Open-Closed Principle—enables flexibility and extensibility. However, the act of creation inherently requires knowing the concrete type:
// Programming to abstraction
PaymentProcessor processor = ...; // What goes here?
// The problem: at some point, someone must say:
PaymentProcessor processor = new StripePaymentProcessor(); // Concrete!
This creates a design dilemma: your code strives to be abstract and flexible, yet object creation ties it to specific concrete classes. Creational patterns exist precisely to isolate and manage this necessary coupling.
Every new keyword creates a compile-time dependency on a concrete class. If your business logic is littered with new ConcreteClass() calls, you've effectively hardwired your system to those implementations. Changing processors, storage mechanisms, or service providers requires modifying code throughout your application—violating the Open-Closed Principle.
The scope of creation complexity:
Object creation becomes architecturally significant when:
Type Selection is Dynamic — The concrete type depends on runtime conditions (user input, configuration, context)
Construction is Complex — Objects require multi-step assembly, dependencies, or validation before use
Instance Control is Required — Only certain quantities of objects should exist (singletons, pools, flyweights)
Creation Varies by Context — Different environments or platforms require different implementations
Object Families Must Coordinate — Related objects must be created together to ensure compatibility
Creational patterns provide principled solutions to each of these challenges, turning object creation from a scattered concern into a well-designed subsystem.
The Gang of Four identified five fundamental creational patterns, each addressing a specific aspect of object creation. These patterns share a common theme: encapsulating knowledge about which concrete classes the system uses while providing flexibility in what, how, and when objects get created.
| Pattern | Primary Problem Solved | Key Mechanism |
|---|---|---|
| Factory Method | Creating objects without specifying exact class | Delegate creation to subclasses via method override |
| Abstract Factory | Creating families of related objects | Interface for creating multiple related object types |
| Builder | Constructing complex objects step-by-step | Separate construction algorithm from representation |
| Prototype | Creating objects by cloning existing instances | Clone mechanism instead of constructor calls |
| Singleton | Ensuring exactly one instance exists | Private constructor with controlled access point |
What unifies creational patterns:
Abstraction of Instantiation — Client code works with interfaces, not concrete creation logic
Centralization of Knowledge — Decisions about which classes to instantiate are localized, not scattered
Flexibility at Creation Time — The system can adapt what it creates without changing how clients request creation
Encapsulation of Complexity — Construction details are hidden from the code that uses objects
Each pattern represents a different balance of these properties, optimized for different problem contexts.
The Problem:
You're building a framework or library that defines a generic workflow, but specific steps require creating objects whose type varies based on the application's needs. You want to define the skeleton in a base class while allowing subclasses to determine which concrete objects to create.
The Solution:
The Factory Method pattern defines an interface for creating an object but lets subclasses decide which class to instantiate. It defers instantiation to subclasses, allowing a class to delegate creation to its descendants.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
// Abstract product interfaceinterface Document { open(): void; save(): void; close(): void;} // Concrete productsclass PDFDocument implements Document { open(): void { console.log("Opening PDF document with PDF renderer"); } save(): void { console.log("Saving PDF with compression"); } close(): void { console.log("Closing PDF document"); }} class WordDocument implements Document { open(): void { console.log("Opening Word document with DOCX parser"); } save(): void { console.log("Saving Word document with formatting"); } close(): void { console.log("Closing Word document"); }} // Abstract creator with factory methodabstract class Application { // Factory method - subclasses decide what to create protected abstract createDocument(): Document; // Template method that uses the factory method public newDocument(): Document { const doc = this.createDocument(); doc.open(); console.log("Document ready for editing"); return doc; }} // Concrete creatorsclass PDFApplication extends Application { protected createDocument(): Document { return new PDFDocument(); }} class WordApplication extends Application { protected createDocument(): Document { return new WordDocument(); }} // Client code works with abstract typesfunction clientCode(app: Application) { const document = app.newDocument(); document.save(); document.close();} // Runtime selection of concrete applicationclientCode(new PDFApplication()); // Creates PDF documentsclientCode(new WordApplication()); // Creates Word documentsDocument interfaceUse Factory Method when: (1) a class can't anticipate which objects it must create, (2) you want subclasses to specify what they create, or (3) you're building a framework where users override creation behavior. It's particularly common in frameworks where the core defines workflows but applications customize the objects involved.
The Problem:
Your system must create families of related objects that are designed to work together. Using objects from different families would cause errors or inconsistencies. You need a way to ensure that when you create a UI button, the scrollbar, checkbox, and window you create are all from the same UI toolkit (Windows, macOS, or Linux).
The Solution:
The Abstract Factory pattern provides an interface for creating families of related objects without specifying their concrete classes. Each concrete factory produces a complete, compatible family of products.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
// Abstract product interfaces for a UI component familyinterface Button { render(): void; onClick(handler: () => void): void;} interface Checkbox { render(): void; isChecked(): boolean;} interface TextInput { render(): void; getValue(): string;} // Abstract factory interfaceinterface UIFactory { createButton(): Button; createCheckbox(): Checkbox; createTextInput(): TextInput;} // Windows family of productsclass WindowsButton implements Button { render(): void { console.log("Rendering Windows-style button"); } onClick(handler: () => void): void { /* Windows click handling */ }} class WindowsCheckbox implements Checkbox { render(): void { console.log("Rendering Windows-style checkbox"); } isChecked(): boolean { return false; }} class WindowsTextInput implements TextInput { render(): void { console.log("Rendering Windows-style text input"); } getValue(): string { return ""; }} // macOS family of productsclass MacOSButton implements Button { render(): void { console.log("Rendering macOS-style button"); } onClick(handler: () => void): void { /* macOS click handling */ }} class MacOSCheckbox implements Checkbox { render(): void { console.log("Rendering macOS-style checkbox"); } isChecked(): boolean { return false; }} class MacOSTextInput implements TextInput { render(): void { console.log("Rendering macOS-style text input"); } getValue(): string { return ""; }} // Concrete factoriesclass WindowsUIFactory implements UIFactory { createButton(): Button { return new WindowsButton(); } createCheckbox(): Checkbox { return new WindowsCheckbox(); } createTextInput(): TextInput { return new WindowsTextInput(); }} class MacOSUIFactory implements UIFactory { createButton(): Button { return new MacOSButton(); } createCheckbox(): Checkbox { return new MacOSCheckbox(); } createTextInput(): TextInput { return new MacOSTextInput(); }} // Client code works only with abstract typesclass LoginForm { private button: Button; private rememberMe: Checkbox; private username: TextInput; private password: TextInput; constructor(factory: UIFactory) { // All components come from the same family this.button = factory.createButton(); this.rememberMe = factory.createCheckbox(); this.username = factory.createTextInput(); this.password = factory.createTextInput(); } render(): void { this.username.render(); this.password.render(); this.rememberMe.render(); this.button.render(); }} // Platform detection at application startfunction createUIFactory(): UIFactory { const platform = process.platform; if (platform === "darwin") { return new MacOSUIFactory(); } return new WindowsUIFactory();} // Single decision point for entire object familyconst factory = createUIFactory();const form = new LoginForm(factory);form.render();Abstract Factory vs. Factory Method:
These patterns often work together but address different concerns:
| Aspect | Factory Method | Abstract Factory |
|---|---|---|
| Scope | Creates one product type | Creates families of products |
| Mechanism | Inheritance (override method) | Composition (inject factory) |
| Focus | Defer creation to subclasses | Ensure product compatibility |
| Variation | One product varies | Multiple products vary together |
In practice, Abstract Factories often use Factory Methods internally—each create* method is essentially a Factory Method.
Abstract Factory is essential in: (1) Cross-platform UI frameworks ensuring visual consistency, (2) Database access layers supporting multiple database engines, (3) Document processing systems handling different file formats, (4) Game engines creating themed entity families. Anywhere you need guaranteed compatibility between related objects, Abstract Factory applies.
The Problem:
You need to create complex objects with many optional components and possible configurations. Using constructors leads to either:
nullThe Solution:
The Builder pattern separates the construction of a complex object from its representation. A Builder constructs the product step-by-step, allowing different representations to be created using the same construction process.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
// Complex product with many optional componentsclass HttpRequest { readonly method: string; readonly url: string; readonly headers: Map<string, string>; readonly body: string | null; readonly timeout: number; readonly retryCount: number; readonly followRedirects: boolean; readonly authentication: { type: string; credentials: string } | null; constructor( method: string, url: string, headers: Map<string, string>, body: string | null, timeout: number, retryCount: number, followRedirects: boolean, authentication: { type: string; credentials: string } | null ) { this.method = method; this.url = url; this.headers = headers; this.body = body; this.timeout = timeout; this.retryCount = retryCount; this.followRedirects = followRedirects; this.authentication = authentication; }} // Fluent builder for step-by-step constructionclass HttpRequestBuilder { private method: string = "GET"; private url: string = ""; private headers: Map<string, string> = new Map(); private body: string | null = null; private timeout: number = 30000; private retryCount: number = 0; private followRedirects: boolean = true; private auth: { type: string; credentials: string } | null = null; setMethod(method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"): this { this.method = method; return this; } setUrl(url: string): this { this.url = url; return this; } addHeader(name: string, value: string): this { this.headers.set(name, value); return this; } setJsonBody(data: object): this { this.body = JSON.stringify(data); this.headers.set("Content-Type", "application/json"); return this; } setTimeout(ms: number): this { this.timeout = ms; return this; } withRetry(count: number): this { this.retryCount = count; return this; } noFollowRedirects(): this { this.followRedirects = false; return this; } withBasicAuth(username: string, password: string): this { const credentials = btoa(`${username}:${password}`); this.auth = { type: "Basic", credentials }; this.headers.set("Authorization", `Basic ${credentials}`); return this; } withBearerToken(token: string): this { this.auth = { type: "Bearer", credentials: token }; this.headers.set("Authorization", `Bearer ${token}`); return this; } build(): HttpRequest { // Validation before construction if (!this.url) { throw new Error("URL is required"); } return new HttpRequest( this.method, this.url, new Map(this.headers), // Defensive copy this.body, this.timeout, this.retryCount, this.followRedirects, this.auth ); }} // Clean, readable constructionconst request = new HttpRequestBuilder() .setMethod("POST") .setUrl("https://api.example.com/users") .setJsonBody({ name: "Alice", role: "admin" }) .withBearerToken("eyJhbGciOiJIUzI1NiIs...") .withRetry(3) .setTimeout(5000) .build();The Problem:
You need to create new objects that are similar to existing objects, but:
The Solution:
The Prototype pattern creates new objects by copying (cloning) an existing object—the prototype. This delegates creation to the objects themselves, avoiding the need to know their concrete classes.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
// Prototype interfaceinterface Cloneable<T> { clone(): T;} // Complex object that's expensive to create from scratchclass GameCharacter implements Cloneable<GameCharacter> { name: string; health: number; level: number; equipment: Map<string, Equipment>; skills: Skill[]; position: { x: number; y: number }; // Expensive initialization private loadedAssets: string[] = []; constructor(name: string) { this.name = name; this.health = 100; this.level = 1; this.equipment = new Map(); this.skills = []; this.position = { x: 0, y: 0 }; // Simulating expensive asset loading this.loadAssets(); } private loadAssets(): void { // In reality, this might load textures, models, sounds... console.log(`Loading assets for ${this.name}...`); this.loadedAssets = ["texture", "model", "animations", "sounds"]; } // Deep clone implementation clone(): GameCharacter { // Create new instance without expensive asset loading const cloned = Object.create(GameCharacter.prototype); // Copy primitive values cloned.name = this.name; cloned.health = this.health; cloned.level = this.level; // Deep copy collections cloned.equipment = new Map( Array.from(this.equipment.entries()).map( ([slot, equip]) => [slot, equip.clone()] ) ); cloned.skills = this.skills.map(skill => skill.clone()); cloned.position = { ...this.position }; // Share expensive assets (they're read-only) cloned.loadedAssets = this.loadedAssets; return cloned; }} // Prototype registry for managing prototypesclass CharacterPrototypeRegistry { private prototypes: Map<string, GameCharacter> = new Map(); register(key: string, prototype: GameCharacter): void { this.prototypes.set(key, prototype); } create(key: string): GameCharacter { const prototype = this.prototypes.get(key); if (!prototype) { throw new Error(`No prototype registered for: ${key}`); } return prototype.clone(); }} // Usage: create prototype templates once, clone many timesconst registry = new CharacterPrototypeRegistry(); // Expensive creation happens once per templateconst warriorTemplate = new GameCharacter("Warrior Template");warriorTemplate.health = 150;warriorTemplate.skills.push(new MeleeSkill("Sword Strike"));warriorTemplate.equipment.set("weapon", new Sword("Iron Sword")); registry.register("warrior", warriorTemplate); // Fast cloning for spawning enemiesconst enemy1 = registry.create("warrior");const enemy2 = registry.create("warrior");enemy1.name = "Enemy Warrior 1";enemy2.name = "Enemy Warrior 2";// Assets loaded once, shared by all clonesThe biggest challenge with Prototype is implementing cloning correctly. Shallow copy duplicates only top-level values—nested objects remain shared. Deep copy recursively clones all nested structures. Choose based on your needs: shared references for read-only data, deep copies for independently mutable state. Circular references require special handling to avoid infinite loops.
The Problem:
Certain resources must have exactly one instance throughout the application's lifetime:
Multiple instances could cause resource conflicts, inconsistent state, or excessive resource consumption.
The Solution:
The Singleton pattern ensures a class has only one instance and provides a global point of access to it. The class itself manages its single instance, preventing external code from creating additional instances.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
// Classic Singleton implementationclass ConfigurationManager { private static instance: ConfigurationManager | null = null; private settings: Map<string, unknown> = new Map(); // Private constructor prevents external instantiation private constructor() { console.log("Loading configuration..."); this.loadFromEnvironment(); } // Controlled access point public static getInstance(): ConfigurationManager { if (!ConfigurationManager.instance) { ConfigurationManager.instance = new ConfigurationManager(); } return ConfigurationManager.instance; } private loadFromEnvironment(): void { // Load from environment, files, etc. this.settings.set("database.host", process.env.DB_HOST); this.settings.set("database.port", parseInt(process.env.DB_PORT || "5432")); this.settings.set("api.timeout", 30000); } get<T>(key: string): T | undefined { return this.settings.get(key) as T | undefined; } // For testing: ability to reset (use with caution) public static resetInstance(): void { ConfigurationManager.instance = null; }} // Usage anywhere in applicationconst config1 = ConfigurationManager.getInstance();const config2 = ConfigurationManager.getInstance();console.log(config1 === config2); // true - same instance // Thread-safe singleton (for environments with true concurrency)class ThreadSafeConnectionPool { private static instance: ThreadSafeConnectionPool | null = null; private static initializationLock = false; private connections: DatabaseConnection[] = []; private constructor(poolSize: number) { for (let i = 0; i < poolSize; i++) { this.connections.push(new DatabaseConnection()); } } public static getInstance(): ThreadSafeConnectionPool { // Double-checked locking pattern if (!ThreadSafeConnectionPool.instance) { if (!ThreadSafeConnectionPool.initializationLock) { ThreadSafeConnectionPool.initializationLock = true; try { if (!ThreadSafeConnectionPool.instance) { ThreadSafeConnectionPool.instance = new ThreadSafeConnectionPool(10); } } finally { ThreadSafeConnectionPool.initializationLock = false; } } } return ThreadSafeConnectionPool.instance!; }}Singleton is often criticized as an anti-pattern because: (1) Global state makes testing difficult—you can't easily substitute mock instances. (2) Hidden dependencies—classes using singletons have non-obvious dependencies. (3) Concurrency complexity—thread-safe initialization is tricky. Consider Dependency Injection as an alternative: inject a single instance rather than having classes fetch it globally. This preserves single-instance semantics while enabling testability.
Modern alternatives to traditional Singleton:
// config.ts
class ConfigurationManager { /* ... */ }
export const config = new ConfigurationManager();
// Other files import the same instance
// Register as singleton in container
container.register(ConfigurationManager, { lifecycle: Lifecycle.Singleton });
These approaches maintain single-instance behavior while improving testability and reducing global state issues.
Selecting the appropriate creational pattern begins with understanding your specific creation challenge. Each pattern optimizes for different scenarios:
| If Your Problem Is... | Consider... | Key Indicator |
|---|---|---|
| Unknown concrete type at compile time | Factory Method | Subclasses should decide what to create |
| Need families of compatible objects | Abstract Factory | Products must be used together consistently |
| Complex object with many configurations | Builder | Telescoping constructors, many optional parts |
| Creating expensive-to-initialize objects | Prototype | Clone from template faster than construct new |
| Must have exactly one instance | Singleton | Global access point, shared resource |
Creational patterns often work together. An Abstract Factory might use Factory Methods internally. A Builder might use a Prototype for its default state. A Factory might return a Singleton for certain types. Don't think of patterns as mutually exclusive—they're complementary tools addressing different aspects of creation.
Creational patterns address the first fundamental challenge in object-oriented design: how do we create objects flexibly and maintainably? By encapsulating creation logic, these patterns decouple systems from the concrete classes they instantiate, enabling the flexibility that SOLID principles demand.
You now understand Creational Patterns—the first category of the Gang of Four taxonomy. Next, we'll explore Structural Patterns, which address how objects and classes combine to form larger structures. Where creational patterns manage what gets created, structural patterns manage how things connect.