Loading learning content...
The Gang of Four catalog describes 23 patterns. Real-world systems use dozens—often simultaneously, interacting in sophisticated ways. The pattern vocabulary is like musical notes; individual notes have meaning, but music emerges from their combination.
Pattern combination is the orchestration skill of software architecture.
A seasoned architect doesn't think 'I'll use Strategy here and Observer there.' They think 'This domain model naturally emerges from State managing workflow, Factory creating state objects, Observer notifying external consumers, and Command capturing operations for audit.' These patterns don't just coexist—they work together, each pattern's outputs becoming another's inputs.
This page teaches you to compose patterns into cohesive architectures. You'll learn which patterns combine naturally, how to identify seams where patterns meet, and how to avoid the complexity explosion that comes from undisciplined pattern stacking.
By the end of this page, you will understand the principles of pattern composition, recognize classic pattern combinations and their applications, learn to identify integration points where patterns meet, and develop the skill of designing multi-pattern architectures that remain coherent and maintainable.
Pattern combination isn't arbitrary—it follows principles that determine whether patterns compose harmoniously or create confusion. Understanding these principles enables deliberate, effective pattern orchestration.
The Fundamental Principle: Orthogonality
Patterns combine most effectively when they address orthogonal concerns—independent dimensions of the problem that don't overlap. When patterns address the same concern, they conflict; when they address independent concerns, they compose cleanly.
Example of Orthogonal Combination:
These concerns are independent. A system can use Factory to create Strategy objects, and those Strategies can be Observers—each pattern handles its own responsibility without confusion.
For any pattern combination, ask: 'Can I explain each pattern's role in one sentence without mentioning the other patterns?' If yes, the combination is likely clean. If explaining one pattern requires explaining others, the concerns may be entangled.
Certain pattern combinations appear so frequently in well-designed systems that they've become recognized idioms. Learning these combinations accelerates your ability to compose patterns effectively.
Why Classic Combinations Matter:
These combinations aren't arbitrary—they reflect natural affinities between patterns. Where one pattern creates a need, another pattern often fills it. Recognizing these affinities enables rapid composition in new domains.
The Combination:
Factory creates Strategy instances; clients work with Strategies without knowing concrete types.
Why They Combine:
Common Scenarios:
12345678910111213141516171819202122232425262728293031323334353637383940414243
// The Strategy interfaceinterface CompressionStrategy { compress(data: Buffer): Buffer; decompress(data: Buffer): Buffer;} // Concrete Strategiesclass GzipCompression implements CompressionStrategy { compress(data: Buffer): Buffer { /* ... */ } decompress(data: Buffer): Buffer { /* ... */ }} class LZ4Compression implements CompressionStrategy { compress(data: Buffer): Buffer { /* ... */ } decompress(data: Buffer): Buffer { /* ... */ }} // Factory that creates Strategiesclass CompressionFactory { createStrategy(format: string): CompressionStrategy { switch (format) { case 'gzip': return new GzipCompression(); case 'lz4': return new LZ4Compression(); default: throw new Error(`Unknown format: ${format}`); } }} // Client uses Factory to get Strategy, works with interfaceclass FileArchiver { constructor( private factory: CompressionFactory, private format: string ) {} archive(files: Buffer[]): Buffer { const strategy = this.factory.createStrategy(this.format); return files.reduce((acc, file) => Buffer.concat([acc, strategy.compress(file)]), Buffer.alloc(0) ); }}When patterns combine, they meet at seams—points of integration where one pattern's output becomes another's input, or where patterns share common elements. Identifying seams is crucial for clean composition.
Types of Pattern Seams:
Designing Clean Seams:
The quality of pattern seams determines the maintainability of the composition.
Clean Seam Characteristics:
Problematic Seam Indicators:
Every seam should have an explicit or implicit 'contract': what one pattern provides, what another expects. Document these contracts for complex compositions. When seam contracts are clear, patterns can evolve independently.
Complex systems require multiple patterns working in concert. Designing these multi-pattern architectures requires a systematic approach that ensures coherence.
The Architecture Design Process:
Case Study: E-Commerce Order System
Let's design a multi-pattern architecture for a realistic order management system:
Requirements:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
PROBLEM DECOMPOSITION:=====================1. Order Lifecycle → STATE pattern (state-dependent behavior, transitions)2. Payment Processing → STRATEGY pattern (interchangeable payment methods)3. Price Modification → DECORATOR pattern (stackable price adjustments)4. Event Notification → OBSERVER pattern (decouple order from listeners)5. Audit Trail → COMMAND pattern (operations as objects for logging)6. Object Creation → FACTORY pattern (create strategies, decorators) SEAM IDENTIFICATION:===================Seam A: State transitions trigger Observer notifications [State] --notifies--> [Observer] Contract: State provides (orderId, oldState, newState) Seam B: Order uses Factory to create PaymentStrategy [Order] --requests--> [Factory] --creates--> [Strategy] Contract: Factory takes paymentMethod, returns PaymentStrategy Seam C: PricingService uses Decorator chain [Factory] --creates--> [Decorator chain] --used by--> [Order] Contract: Decorators implement PriceCalculator interface Seam D: State operations wrapped in Commands for audit [Command] --wraps--> [State transition] Contract: Command captures operation details for logging PATTERN ORCHESTRATION:=====================OrderService (coordinates the patterns)├── OrderFactory (creates Order with initial State)├── PaymentStrategyFactory (creates payment processors)├── PriceDecoratorFactory (builds price modification chain)├── OrderStateObservable (manages Observer subscriptions)└── CommandInvoker (executes and logs Commands) Order├── Has current State (State pattern)├── Uses PaymentStrategy (Strategy pattern)├── Uses PriceCalculator (Decorator chain)└── Notifies OrderStateObservable on transitions CHANGE SCENARIO VALIDATION:==========================Q: Add new payment method (e.g., crypto)?A: Add new Strategy, register in Factory. No other patterns affected. Q: Add new price modifier (e.g., flash sale)?A: Add new Decorator, include in chain. Other decorators unchanged. Q: Add new notification channel (e.g., SMS)?A: Add new Observer. Order unchanged. Q: Add new order state (e.g., OnHold)?A: Add new State class, update transitions. Commands/Observers work automatically.Pattern combinations can go wrong in characteristic ways. Recognizing these anti-patterns helps avoid over-complicated or dysfunctional architectures.
Common Combination Mistakes:
Every project has a 'complexity budget'—the total amount of complexity the team can effectively manage. Pattern combinations consume this budget. Before adding another pattern to a composition, ask: Is the benefit worth the complexity cost? What simpler alternative was considered?
Multi-pattern architectures require thoughtful testing strategies. The good news: well-composed patterns are inherently testable because concerns are separated.
Testing Strategy for Pattern Combinations:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
// Level 1: Unit test pattern participantdescribe('GzipCompression (Strategy participant)', () => { it('should compress and decompress symmetrically', () => { const strategy = new GzipCompression(); const original = Buffer.from('test data'); const compressed = strategy.compress(original); const decompressed = strategy.decompress(compressed); expect(decompressed).toEqual(original); });}); // Level 2: Test pattern mechanicsdescribe('CompressionFactory (Factory pattern)', () => { it('should return GzipCompression for gzip format', () => { const factory = new CompressionFactory(); const strategy = factory.createStrategy('gzip'); expect(strategy).toBeInstanceOf(GzipCompression); }); it('should throw for unknown format', () => { const factory = new CompressionFactory(); expect(() => factory.createStrategy('unknown')) .toThrow('Unknown format'); });}); // Level 3: Test seam contractdescribe('Factory + Strategy seam', () => { it('should create strategy that implements interface correctly', () => { const factory = new CompressionFactory(); // Factory creates strategy const strategy = factory.createStrategy('gzip'); // Contract: strategy must implement compress/decompress expect(typeof strategy.compress).toBe('function'); expect(typeof strategy.decompress).toBe('function'); // Contract: methods must be callable with Buffer expect(() => strategy.compress(Buffer.from('test'))).not.toThrow(); });}); // Level 4: Integration test compositiondescribe('FileArchiver (Factory + Strategy composition)', () => { it('should archive multiple files using selected strategy', () => { const archiver = new FileArchiver(new CompressionFactory(), 'gzip'); const files = [Buffer.from('file1'), Buffer.from('file2')]; const archive = archiver.archive(files); // Integration test: Factory, Strategy, and Archiver work together expect(archive.length).toBeGreaterThan(0); });}); // Level 5: Scenario-based testdescribe('User archives project with selected compression', () => { it('should produce smaller archive with compression', () => { const archiver = new FileArchiver(new CompressionFactory(), 'gzip'); const largeFile = Buffer.alloc(10000, 'a'); const archive = archiver.archive([largeFile]); expect(archive.length).toBeLessThan(largeFile.length); });});Real systems evolve. Requirements change. What started as a simple Factory + Strategy might need Observer for event handling. A State machine might need Command for audit. The ability to evolve pattern compositions is a key skill.
Incremental Pattern Addition:
Patterns should be addable without rewriting the system. This is possible when:
Evolution Strategies:
The best pattern compositions are designed to evolve. They have clear extension points, follow the Open-Closed Principle, and use interfaces that can accommodate future patterns. When you design a composition, ask: 'Where might we need to add patterns later?' and ensure those seams are clean.
Pattern combination is where single patterns become architectural vocabulary. Let's consolidate the key insights:
What's next:
With pattern matching, selection, and combination skills, there's one critical skill remaining: knowing when to stop. The next page addresses avoiding over-engineering—the discipline of restraint that prevents pattern enthusiasm from producing unmaintainable complexity.
You now understand how patterns compose into architectures, from classic combinations to multi-pattern system design. In the final page of this module, we'll develop the critical skill of knowing when patterns become over-engineering.