Loading content...
Individual patterns are powerful, but pattern combinations are transformative. Real-world systems rarely use patterns in isolation—they compose them to solve complex design challenges that no single pattern can address.
Combining patterns isn't about stacking complexity. When done well, pattern combinations create elegant synergies where each pattern addresses a distinct concern, and together they form a cohesive architecture. The whole becomes greater than the sum of its parts.
This page explores the art and science of combining creational patterns—with each other and with structural and behavioral patterns. You'll learn common combinations, understand their synergies, and develop intuition for when composition is appropriate.
By the end of this page, you will understand common creational pattern combinations, recognize synergies between patterns, know how to compose patterns to solve complex problems, and avoid the pitfall of over-combining. You'll see patterns as composable building blocks rather than isolated solutions.
Before diving into specific combinations, let's understand why patterns are combined. Each pattern solves a specific problem, but real design challenges often involve multiple interrelated problems.
Common reasons to combine patterns:
The composability principle:
Patterns compose well when they address orthogonal concerns. If two patterns try to solve the same problem in different ways, combining them creates confusion. If they solve different problems, combining them creates synergy.
For example:
More patterns isn't better. Each pattern adds conceptual weight. Combine patterns only when each is genuinely solving a distinct problem. If your design requires mental gymnastics to understand, you've likely over-combined.
Combining creational patterns with each other is common when one pattern addresses what to create and another addresses how to create it, or when different lifecycle concerns overlap.
Common Creational + Creational combinations:
| Combination | Use Case | Synergy |
|---|---|---|
| Abstract Factory + Builder | Factory creates families of complex objects | Factory decides what; Builder handles step-by-step construction |
| Abstract Factory + Prototype | Factory uses cloning instead of new() | Factory interface; Prototype handles creation mechanism |
| Factory + Singleton | Factory itself is a singleton | Global access to factory; factory handles creation |
| Builder + Prototype | Builder clones a template then customizes | Prototype provides base; Builder adds customizations |
| Object Pool + Prototype | Pool creates instances by cloning | Pool manages lifecycle; Prototype handles initialization |
Let's explore the most powerful combinations in detail:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
// COMBINATION: Abstract Factory + Builder// Factory decides WHAT to build; Builder handles HOW to build it interface DocumentBuilder { setTitle(title: string): this; addSection(section: Section): this; addTable(table: Table): this; setFooter(footer: string): this; build(): Document;} interface DocumentFactory { createDocumentBuilder(): DocumentBuilder; createTableBuilder(): TableBuilder; createChartBuilder(): ChartBuilder;} // PDF familyclass PdfDocumentFactory implements DocumentFactory { createDocumentBuilder(): DocumentBuilder { return new PdfDocumentBuilder(); // PDF-specific builder } createTableBuilder(): TableBuilder { return new PdfTableBuilder(); } createChartBuilder(): ChartBuilder { return new PdfChartBuilder(); }} // HTML familyclass HtmlDocumentFactory implements DocumentFactory { createDocumentBuilder(): DocumentBuilder { return new HtmlDocumentBuilder(); // HTML-specific builder } createTableBuilder(): TableBuilder { return new HtmlTableBuilder(); } createChartBuilder(): ChartBuilder { return new HtmlChartBuilder(); }} // Usage: Factory provides family-specific buildersfunction createReport(factory: DocumentFactory, data: ReportData) { const docBuilder = factory.createDocumentBuilder(); const tableBuilder = factory.createTableBuilder(); const table = tableBuilder .setHeaders(data.headers) .addRows(data.rows) .build(); return docBuilder .setTitle(data.title) .addSection(data.introduction) .addTable(table) .setFooter(data.footer) .build();}Abstract Factory + Builder is particularly powerful for generating complex documents, configurations, or data structures in multiple formats. The factory ensures format consistency; the builder handles construction complexity.
Creational patterns often combine with structural patterns. The creational pattern handles how objects are created, while the structural pattern handles how objects are composed.
Common Creational + Structural combinations:
| Combination | Use Case | Synergy |
|---|---|---|
| Factory + Decorator | Factory creates decorated objects | Factory assembles decorator chain transparently |
| Factory + Composite | Factory creates composite hierarchies | Factory builds tree structure; Composite provides uniformity |
| Factory + Adapter | Factory returns adapted interfaces | Client sees uniform interface; adapters hide differences |
| Factory + Proxy | Factory returns proxies instead of real objects | Transparent interception (lazy load, access control, logging) |
| Builder + Composite | Builder constructs composite structures | Step-by-step tree construction |
| Singleton + Facade | Facade is a singleton | Single entry point to complex subsystem |
Deep dive: Factory + Decorator
This combination is particularly powerful for building configurable processing pipelines:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
// COMBINATION: Factory + Decorator// Factory assembles decorator chains based on configuration interface DataStream { read(): Buffer; write(data: Buffer): void;} // Concrete componentclass FileStream implements DataStream { read(): Buffer { /* ... */ } write(data: Buffer): void { /* ... */ }} // Decoratorsclass CompressionDecorator implements DataStream { constructor(private wrapped: DataStream) {} read(): Buffer { const data = this.wrapped.read(); return decompress(data); } write(data: Buffer): void { this.wrapped.write(compress(data)); }} class EncryptionDecorator implements DataStream { constructor(private wrapped: DataStream, private key: CryptoKey) {} read(): Buffer { const data = this.wrapped.read(); return decrypt(data, this.key); } write(data: Buffer): void { this.wrapped.write(encrypt(data, this.key)); }} class BufferingDecorator implements DataStream { constructor(private wrapped: DataStream, private bufferSize: number) {} read(): Buffer { /* buffered read */ } write(data: Buffer): void { /* buffered write */ }} // Factory assembles the decorator chainclass DataStreamFactory { createStream(config: StreamConfig): DataStream { let stream: DataStream = new FileStream(config.path); // Layer decorators based on configuration if (config.buffered) { stream = new BufferingDecorator(stream, config.bufferSize || 8192); } if (config.compressed) { stream = new CompressionDecorator(stream); } if (config.encrypted) { stream = new EncryptionDecorator(stream, config.encryptionKey); } return stream; }} // Usage: Client doesn't know about decorator chainconst factory = new DataStreamFactory();const stream = factory.createStream({ path: '/data/sensitive.dat', buffered: true, compressed: true, encrypted: true, encryptionKey: myKey}); // Client just uses the streamstream.write(myData); // Transparently buffered, compressed, encryptedThis pattern is everywhere: HTTP clients with middleware layers, logging with formatters and transporters, data pipelines with transformations. The factory reads configuration and assembles the appropriate decorator chain, hiding complexity from clients.
Creational patterns also combine naturally with behavioral patterns. The creational pattern creates objects with the right structure; the behavioral pattern defines how those objects interact or behave.
Common Creational + Behavioral combinations:
| Combination | Use Case | Synergy |
|---|---|---|
| Factory + Strategy | Factory creates appropriate strategy | Decouple strategy selection from usage |
| Factory + Command | Factory creates commands | Decouple command creation from execution |
| Factory + State | Factory creates state objects | Decouple state creation from state machine |
| Builder + Template Method | Template method uses builder | Algorithm structure fixed; builder varies output |
| Prototype + Memento | Clone to create snapshots | Cloning mechanism serves as memento creation |
| Singleton + Observer | Singleton as event/notification hub | Central observable for system-wide events |
Deep dive: Factory + Strategy
This is one of the most common and powerful combinations. The factory selects and creates the appropriate strategy based on context:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
// COMBINATION: Factory + Strategy// Factory creates the appropriate strategy based on context interface PaymentStrategy { processPayment(amount: number, details: PaymentDetails): PaymentResult; validate(details: PaymentDetails): ValidationResult;} class CreditCardStrategy implements PaymentStrategy { processPayment(amount: number, details: PaymentDetails): PaymentResult { // Process via credit card gateway } validate(details: PaymentDetails): ValidationResult { // Validate card number, CVV, expiry }} class PayPalStrategy implements PaymentStrategy { processPayment(amount: number, details: PaymentDetails): PaymentResult { // Redirect to PayPal, handle callback } validate(details: PaymentDetails): ValidationResult { // Validate PayPal email }} class CryptoStrategy implements PaymentStrategy { processPayment(amount: number, details: PaymentDetails): PaymentResult { // Generate wallet address, verify blockchain } validate(details: PaymentDetails): ValidationResult { // Validate wallet address format }} // Factory creates appropriate strategyclass PaymentStrategyFactory { private strategies = new Map<string, () => PaymentStrategy>([ ['credit_card', () => new CreditCardStrategy()], ['paypal', () => new PayPalStrategy()], ['crypto', () => new CryptoStrategy()], ]); createStrategy(method: string): PaymentStrategy { const creator = this.strategies.get(method); if (!creator) { throw new Error(`Unsupported payment method: ${method}`); } return creator(); } // Factory can also consider context createStrategyForOrder(order: Order): PaymentStrategy { // Select strategy based on order properties if (order.total > 10000 && order.customer.cryptoEnabled) { return this.createStrategy('crypto'); } if (order.customer.preferredMethod) { return this.createStrategy(order.customer.preferredMethod); } return this.createStrategy('credit_card'); // Default }} // Usage: Service uses factory to get strategyclass PaymentService { constructor(private strategyFactory: PaymentStrategyFactory) {} processOrder(order: Order): PaymentResult { const strategy = this.strategyFactory.createStrategyForOrder(order); const validation = strategy.validate(order.paymentDetails); if (!validation.valid) { throw new PaymentValidationError(validation.errors); } return strategy.processPayment(order.total, order.paymentDetails); }}This combination completely decouples strategy selection from strategy usage. The service doesn't know which strategies exist or how to choose them. The factory encapsulates all selection logic, making it easy to add new strategies or change selection rules.
In complex systems, multiple patterns work together to form cohesive architectures. Let's examine a complete example that combines several patterns:
Case Study: Plugin-Based Application Framework
Imagine a content management system that supports pluggable content renderers, storage backends, and caching strategies. This requires multiple patterns working in concert.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
// MULTI-PATTERN ARCHITECTURE: Plugin-Based CMS// Combines: Abstract Factory, Builder, Strategy, Singleton, Decorator // --- ABSTRACT FACTORY: Creates families of plugins ---interface PluginFactory { createRenderer(): ContentRenderer; createStorage(): StorageBackend; createCache(): CacheStrategy;} class WordPressPluginFactory implements PluginFactory { createRenderer(): ContentRenderer { return new WPRenderer(); } createStorage(): StorageBackend { return new WPDatabase(); } createCache(): CacheStrategy { return new WPCache(); }} class HeadlessPluginFactory implements PluginFactory { createRenderer(): ContentRenderer { return new MarkdownRenderer(); } createStorage(): StorageBackend { return new S3Storage(); } createCache(): CacheStrategy { return new RedisCache(); }} // --- BUILDER: Constructs the application ---class CMSBuilder { private pluginFactory: PluginFactory; private middlewares: Middleware[] = []; private config: CMSConfig = new CMSConfig(); usePluginFactory(factory: PluginFactory): this { this.pluginFactory = factory; return this; } addMiddleware(middleware: Middleware): this { this.middlewares.push(middleware); return this; } configure(options: Partial<CMSConfig>): this { Object.assign(this.config, options); return this; } build(): CMS { // DECORATOR: Wrap storage with caching const storage = this.pluginFactory.createStorage(); const cache = this.pluginFactory.createCache(); const cachedStorage = new CachedStorageDecorator(storage, cache); // STRATEGY: Renderer is a strategy for content output const renderer = this.pluginFactory.createRenderer(); // Assemble the CMS return new CMS(cachedStorage, renderer, this.middlewares, this.config); }} // --- SINGLETON: Application container ---class ApplicationContainer { private static instance: ApplicationContainer; private cms: CMS | null = null; private constructor() {} static getInstance(): ApplicationContainer { if (!ApplicationContainer.instance) { ApplicationContainer.instance = new ApplicationContainer(); } return ApplicationContainer.instance; } initialize(cms: CMS): void { if (this.cms) { throw new Error('Application already initialized'); } this.cms = cms; } getCMS(): CMS { if (!this.cms) { throw new Error('Application not initialized'); } return this.cms; }} // --- USAGE: Composing the patterns ---async function bootstrap() { const pluginFactory = loadPluginFactory(process.env.CMS_MODE); const cms = new CMSBuilder() .usePluginFactory(pluginFactory) .addMiddleware(new AuthMiddleware()) .addMiddleware(new LoggingMiddleware()) .addMiddleware(new RateLimitMiddleware()) .configure({ maxUploadSize: 10 * 1024 * 1024, defaultLanguage: 'en', }) .build(); ApplicationContainer.getInstance().initialize(cms); await cms.start();}Pattern roles in this architecture:
| Pattern | Role |
|---|---|
| Abstract Factory | Creates consistent plugin families (WP vs Headless) |
| Builder | Assembles CMS with fluent configuration |
| Decorator | Wraps storage with transparent caching |
| Strategy | Renderer determines content output format |
| Singleton | Application container provides global access |
Each pattern addresses a distinct concern, and together they create a flexible, extensible architecture.
Notice how each pattern has a clear, single responsibility. The Abstract Factory doesn't do building; the Builder doesn't manage instances. This separation is what makes the architecture comprehensible despite using five patterns.
Modern applications often use dependency injection (DI) containers. This changes how creational patterns are applied. Rather than managing creation directly, patterns often configure the DI container or implement its abstractions.
How DI changes pattern application:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
// Pattern application with DI container (using tsyringe as example) import { container, singleton, injectable, inject } from 'tsyringe'; // SINGLETON via DI: No custom singleton code!@singleton@injectableclass ConfigurationService { private config: Config; constructor() { this.config = loadConfig(); } get(key: string): string { return this.config[key]; }} // FACTORY via DI: Register factory functioninterface PaymentProcessor { process(payment: Payment): Promise<PaymentResult>;} container.register<PaymentProcessor>('PaymentProcessor', { useFactory: (c) => { const config = c.resolve(ConfigurationService); const type = config.get('payment.processor'); switch (type) { case 'stripe': return c.resolve(StripeProcessor); case 'paypal': return c.resolve(PayPalProcessor); default: throw new Error(`Unknown processor: ${type}`); } }}); // ABSTRACT FACTORY via DI: Inject factory per familyinterface UIFactory { createButton(): Button; createDialog(): Dialog;} @injectableclass WebUIFactory implements UIFactory { createButton(): Button { return new HtmlButton(); } createDialog(): Dialog { return new HtmlDialog(); }} @injectableclass DesktopUIFactory implements UIFactory { createButton(): Button { return new NativeButton(); } createDialog(): Dialog { return new NativeDialog(); }} // Register based on environmentif (process.env.PLATFORM === 'web') { container.register<UIFactory>('UIFactory', { useClass: WebUIFactory });} else { container.register<UIFactory>('UIFactory', { useClass: DesktopUIFactory });} // Services just inject what they need@injectableclass FormBuilder { constructor( @inject('UIFactory') private uiFactory: UIFactory, private configService: ConfigurationService ) {} buildLoginForm(): Form { const submitButton = this.uiFactory.createButton(); submitButton.setText(this.configService.get('login.buttonText')); // ... }}In DI contexts, you often don't write explicit pattern code. Singleton becomes a registration option. Factories become registered functions. The container handles wiring. Your focus shifts to designing interfaces and registering implementations.
Combining patterns incorrectly creates complexity without benefit. Here are common anti-patterns to avoid:
// Too many patterns for simple object
class UserFactory {
private static instance: UserFactory; // Singleton
static getInstance() { /*...*/ }
createUserBuilder() { // Factory
return new UserBuilder();
}
}
class UserBuilder { // Builder
build(): User {
return this.prototype.clone(); // Prototype?!
}
}
User probably just needs a constructor!
// Simple: Just use constructor
class User {
constructor(
public name: string,
public email: string
) {}
}
// Or if validation needed:
function createUser(data: UserDTO): User {
validate(data);
return new User(data.name, data.email);
}
No pattern needed for simple cases.
The simplicity test:
Before adding a pattern combination, ask:
If answers are unclear, simplify.
We've explored how creational patterns combine with each other and with structural and behavioral patterns. Let's consolidate the principles:
What's next:
The final page presents real-world examples—complete case studies from production systems that demonstrate how creational patterns are applied in practice. You'll see how the theoretical concepts we've covered translate into actual codebases.
You now understand how to combine creational patterns effectively. You know the common combinations, their synergies, and the anti-patterns to avoid. You can design multi-pattern architectures where each pattern has a clear, distinct role. Next, we'll see these concepts in real-world practice.