Loading content...
You've understood that patterns are solutions to problems—not decorations or demonstrations of sophistication. But understanding this principle is only half the battle. The other half is developing the pattern recognition skill: the ability to look at a design challenge and quickly identify which patterns (if any) address it.
This skill separates fluent pattern users from those who fumble through catalogs hoping to find something that fits. The difference is substantial:
We'll develop systematic approaches to problem-pattern matching: how to decompose problems into pattern-relevant characteristics, how to build a mental taxonomy of pattern applicability, and how to rapidly evaluate whether a potential pattern match is genuine.
Pattern matching begins with understanding what kind of problem you have. Design problems fall into recognizable categories, and each category maps to specific pattern families. By decomposing your problem into its essential characteristics, you narrow the pattern search space dramatically.
Most design problems fall into one of these fundamental categories:
| Problem Category | Core Question | Pattern Family |
|---|---|---|
| Object Creation | How should objects be instantiated? | Creational Patterns (Factory, Builder, Prototype, Singleton) |
| Object Structure | How should objects be composed or organized? | Structural Patterns (Adapter, Bridge, Composite, Decorator, Facade, Proxy) |
| Object Behavior | How should objects interact and distribute responsibilities? | Behavioral Patterns (Strategy, Observer, Command, State, Template Method) |
| Object Lifecycle | How should object state and lifetime be managed? | State, Memento, Object Pool, Flyweight |
| Algorithm Variation | How should varying algorithms be encapsulated? | Strategy, Template Method, Visitor |
| Interface Incompatibility | How should incompatible interfaces work together? | Adapter, Facade, Bridge |
| Notification & Events | How should objects learn about state changes? | Observer, Mediator, Event Sourcing |
Every pattern-worthy problem contains a design tension—a conflict between how you'd naturally write the code and requirements that make the natural approach problematic.
Common design tensions include:
Practice naming the tension in problems you encounter. If you can say 'The tension here is between X and Y,' you've taken the first step toward pattern matching. If you can't articulate the tension, you may not have a pattern-worthy problem.
Each pattern addresses problems with distinctive characteristics—a problem signature that acts as a fingerprint. Learning to recognize these signatures enables rapid pattern association.
| Pattern | Problem Signature | Key Indicators |
|---|---|---|
| Factory Method | Need to create objects without specifying exact class | • Type determined at runtime\n• Client should be decoupled from concrete types\n• Creation logic non-trivial |
| Abstract Factory | Need to create families of related objects consistently | • Multiple related objects created together\n• Family must be consistent\n• Platform/configuration varies families |
| Builder | Complex object construction with many parameters | • Many optional parameters\n• Step-by-step construction logic\n• Same process, different representations |
| Prototype | Creating objects by copying existing instances | • Costly to create from scratch\n• Runtime-determined object configuration\n• Need to avoid class dependency |
| Singleton | Exactly one instance with global access point | • Shared resource access\n• Coordination point needed\n• Expensive to create/destroy repeatedly |
| Pattern | Problem Signature | Key Indicators |
|---|---|---|
| Adapter | Interface of existing class doesn't match what's needed | • Integrating third-party libraries\n• Legacy code with incompatible interface\n• Can't modify the adaptee |
| Bridge | Abstraction and implementation should vary independently | • Multiple dimensions of variation\n• Avoiding inheritance explosion\n• Platform-independent abstraction |
| Composite | Tree structure where individual and composite treated uniformly | • Part-whole hierarchies\n• Recursive structure\n• Operations on groups same as individuals |
| Decorator | Add responsibilities dynamically without subclassing | • Features combine in many ways\n• Subclass explosion otherwise\n• Need to add/remove at runtime |
| Facade | Complex subsystem needs a simplified interface | • Many classes to coordinate\n• Clients need simple entry point\n• Hiding complexity from clients |
| Proxy | Control access to another object | • Lazy initialization needed\n• Access control required\n• Remote object access\n• Logging/caching on access |
| Pattern | Problem Signature | Key Indicators |
|---|---|---|
| Strategy | Algorithm should vary independently of the clients | • Multiple algorithms for same operation\n• Algorithm selected at runtime\n• Avoid conditional statements for selection |
| Observer | One object changes, others must be notified | • One-to-many dependency\n• Unknown number of dependents\n• Loose coupling needed |
| Command | Encapsulate requests as objects | • Queue, log, or undo operations\n• Parameterize objects with operations\n• Decouple invoker from performer |
| State | Object behavior changes based on internal state | • Complex state-dependent behavior\n• Many conditionals on state\n• State transitions at runtime |
| Template Method | Algorithm steps fixed, but some steps vary | • Invariant parts of algorithm\n• Customizable hooks\n• Avoid code duplication |
| Chain of Responsibility | Multiple objects may handle a request | • Handler not known a priori\n• Handler determined at runtime\n• Multiple handlers possible |
With problem decomposition skills and pattern signatures in hand, here's a systematic process for matching problems to patterns:
Let's apply this process to a real scenario:
You're building an e-commerce platform that must support multiple payment methods: credit cards, PayPal, Apple Pay, and cryptocurrency. Each has different integration requirements, and merchants can configure which methods they support. New payment methods will be added as they become popular.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
// Step 1: Describe in plain language// "I need to process payments through different providers.// The provider is determined by user choice and merchant config.// Each provider has different APIs and requirements.// I want to add new providers without modifying existing code." // Step 2: Identify core category// This is about BEHAVIOR - how payment processing happens// (Not about creation or structure) // Step 3: Articulate the design tension// Tension: I need a uniform way to handle payments, but the // implementations are all different. I also need extensibility. // Step 4: Match against signatures// Looking at behavioral patterns...// - Strategy: "Algorithm should vary independently of clients"// ✓ Multiple algorithms (payment methods) for same operation (process payment)// ✓ Algorithm selected at runtime (user/merchant choice)// ✓ Avoid conditionals for method selection// // This matches! But let's check one more...// - Factory might also apply if the issue is CREATING processors// → It is, but the core problem is the varying BEHAVIOR // Step 5: Verify the match// Strategy applicability (from GoF):// - "Many related classes differ only in their behavior" ✓// - "You need different variants of an algorithm" ✓// - "An algorithm uses data a client shouldn't know about" ✓// - "A class defines many behaviors as conditionals" ✓ // CONCLUSION: Strategy pattern is appropriate// (Factory may complement it for creating strategy instances) interface PaymentProcessor { processPayment(amount: Money, context: PaymentContext): Promise<PaymentResult>; validatePaymentDetails(details: PaymentDetails): ValidationResult; supportsRefund(): boolean; processRefund(paymentId: string, amount: Money): Promise<RefundResult>;} class StripePaymentProcessor implements PaymentProcessor { constructor(private readonly stripeClient: StripeAPI) {} async processPayment(amount: Money, context: PaymentContext): Promise<PaymentResult> { const charge = await this.stripeClient.charges.create({ amount: amount.cents, currency: amount.currency, source: context.stripeToken, }); return { success: true, transactionId: charge.id }; } validatePaymentDetails(details: PaymentDetails): ValidationResult { // Stripe-specific validation return { valid: true }; } supportsRefund(): boolean { return true; } async processRefund(paymentId: string, amount: Money): Promise<RefundResult> { const refund = await this.stripeClient.refunds.create({ charge: paymentId, amount: amount.cents, }); return { success: true, refundId: refund.id }; }} class PayPalPaymentProcessor implements PaymentProcessor { // PayPal-specific implementation} class CryptoPaymentProcessor implements PaymentProcessor { // Cryptocurrency-specific implementation } // Usage in checkout serviceclass CheckoutService { constructor( private readonly processorFactory: PaymentProcessorFactory ) {} async checkout(cart: Cart, method: PaymentMethod): Promise<OrderResult> { const processor = this.processorFactory.getProcessor(method); const validation = processor.validatePaymentDetails(cart.paymentDetails); if (!validation.valid) { throw new InvalidPaymentDetailsError(validation.errors); } const result = await processor.processPayment(cart.total, cart.context); return this.completeOrder(cart, result); }}Even with systematic processes, pattern matching has pitfalls. Here are the most common mistakes and how to avoid them:
When unsure between similar patterns, ask: 'What is the core problem each pattern solves?' If you can articulate this distinction, you'll match correctly. Strategy: varying algorithms. State: state-dependent behavior changes. Decorator: adding responsibilities. Adapter: interface translation.
Pattern matching ultimately becomes intuitive—experienced engineers 'see' patterns in problems without conscious analysis. This intuition develops through deliberate practice:
Take well-designed open-source projects and analyze their pattern usage:
Certain code smells suggest pattern opportunities. Learn to recognize them:
| Code Smell | What It Suggests | Potential Pattern |
|---|---|---|
| Switch/if-else on type | Type-dependent behavior | Strategy, State, or polymorphism |
| Subclass explosion | Combinations of variations | Decorator, Bridge, or Strategy |
| Complex object construction | Too many constructor parameters | Builder |
new scattered throughout code | Tight coupling to concrete classes | Factory Method, Abstract Factory |
| Many classes referencing each other | High coupling, change ripples | Mediator, Facade |
| Checking null before operations | Optional behavior or lazy loading | Null Object, Proxy |
| Duplicate code in hierarchies | Template with variation points | Template Method |
Given a problem description, practice the matching process:
E-commerce discount system: Discounts can be percentage-based, fixed-amount, buy-one-get-one, or tiered. They can stack in specific ways.
Document editor undo/redo: Users should be able to undo any action and redo undone actions. Some actions are compound (multiple operations).
Plugin system for IDE: Third parties should be able to add new functionality without modifying core code. Plugins can extend multiple extension points.
What feels like intuition is actually pattern matching against accumulated examples. The more problems you analyze, the more problem shapes you recognize. There's no shortcut—only deliberate practice over time.
Real problems often require multiple patterns working together. Recognizing these compound scenarios is an advanced skill that comes with experience.
| Combination | Use Case | How They Complement |
|---|---|---|
| Factory + Strategy | Creating varying algorithm instances | Factory creates the appropriate Strategy implementation |
| Composite + Visitor | Operations on tree structures | Composite defines structure; Visitor adds operations |
| Observer + Command | Undoable event-driven systems | Observer notifies; Command encapsulates undoable actions |
| Decorator + Strategy | Configurable, stackable behaviors | Decorators add responsibilities; Strategies vary algorithms |
| Factory + Prototype | Complex object hierarchies | Factory determines what to create; Prototype clones it |
| State + Singleton | Application-wide state machine | State manages transitions; Singleton ensures single state context |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
// Example: Factory + Strategy + Decorator combination// Problem: Report generation with multiple formats, optional features, // and runtime format selection // Strategy: Different output formatsinterface ReportFormatter { format(data: ReportData): string;} class PDFFormatter implements ReportFormatter { format(data: ReportData): string { /* PDF generation */ }} class ExcelFormatter implements ReportFormatter { format(data: ReportData): string { /* Excel generation */ }} // Decorator: Optional features layered on topabstract class ReportFormatterDecorator implements ReportFormatter { constructor(protected readonly wrapped: ReportFormatter) {} format(data: ReportData): string { return this.wrapped.format(data); }} class EncryptedFormatter extends ReportFormatterDecorator { format(data: ReportData): string { const output = this.wrapped.format(data); return this.encrypt(output); } private encrypt(content: string): string { /* encryption logic */ }} class CompressedFormatter extends ReportFormatterDecorator { format(data: ReportData): string { const output = this.wrapped.format(data); return this.compress(output); } private compress(content: string): string { /* compression logic */ }} // Factory: Creates the appropriate stack based on configurationclass ReportFormatterFactory { create(config: ReportConfig): ReportFormatter { // Base formatter from strategy selection let formatter: ReportFormatter = this.createBaseFormatter(config.format); // Layer decorators based on options if (config.encrypted) { formatter = new EncryptedFormatter(formatter); } if (config.compressed) { formatter = new CompressedFormatter(formatter); } return formatter; } private createBaseFormatter(format: FormatType): ReportFormatter { switch (format) { case 'pdf': return new PDFFormatter(); case 'excel': return new ExcelFormatter(); default: throw new Error(`Unknown format: ${format}`); } }} // Usage: Clean client code despite underlying complexityconst factory = new ReportFormatterFactory();const formatter = factory.create({ format: 'pdf', encrypted: true, compressed: true });const output = formatter.format(reportData);Multi-pattern solutions are powerful but carry multiplicative complexity. Only combine patterns when each solves a distinct, genuine problem. 'We might need this flexibility someday' is not justification for pattern stacking.
Pattern matching is a learnable skill that transforms pattern knowledge from academic to practical. Let's consolidate the key insights:
Next up: With pattern matching skills developing, we'll explore the dangers of premature pattern application—why waiting to introduce patterns is often wiser than applying them early, and how to recognize when the time is right.
You now have a systematic approach to matching problems with patterns. The five-step process—describe, categorize, identify tension, match signatures, verify—transforms pattern selection from guesswork into disciplined analysis. Next, we'll explore why premature patterns are dangerous and how to time pattern introduction correctly.