Strategy Pattern
Define a family of interchangeable algorithms behind a common interface so the calling context can swap them at runtime without changing its own code.
★★★★★4/5Inside a codebase — classes, modules, files
How it works
The Strategy pattern, one of the 23 GoF design patterns (1994), solves a common problem: a context needs to perform a task in multiple ways, and which way to use should be a runtime decision. Without Strategy, this leads to large if/else or switch chains that grow every time a new variant is needed.
The solution: extract each algorithm variant into its own class implementing a common interface (the strategy interface). The context stores a reference to a strategy and delegates the work to it. At runtime, the caller sets the strategy based on whatever condition applies.
In Go, strategies are idiomatically expressed as function types — no interface struct needed. In Rust, trait objects (Box<dyn Strategy>) or enums provide similar flexibility. The key benefit in all languages: adding a new variant means writing a new class/impl, not modifying the context.
Project structure
recommended layoutsrc/├── strategies/│ ├── pricing-strategy.ts # Interface / contract│ ├── standard-pricing.ts # Regular customers│ ├── premium-pricing.ts # Subscribers — 20% off│ └── bulk-pricing.ts # Volume discounts└── checkout-service.ts # Context — holds and uses the strategy
Implementation
TypeScript · Go · Rust// strategies/pricing-strategy.ts
export interface PricingStrategy {
calculate(basePrice: number, quantity: number): number;
}
// strategies/standard-pricing.ts
export class StandardPricing implements PricingStrategy {
calculate(basePrice: number, quantity: number): number {
return basePrice * quantity;
}
}
// strategies/premium-pricing.ts
export class PremiumPricing implements PricingStrategy {
constructor(private discountRate = 0.2) {}
calculate(basePrice: number, quantity: number): number {
return basePrice * quantity * (1 - this.discountRate);
}
}
// strategies/bulk-pricing.ts
export class BulkPricing implements PricingStrategy {
calculate(basePrice: number, quantity: number): number {
const discount = quantity >= 100 ? 0.3 : quantity >= 50 ? 0.15 : 0;
return basePrice * quantity * (1 - discount);
}
}
// checkout-service.ts — Context: holds strategy, delegates to it
export class CheckoutService {
constructor(private pricing: PricingStrategy) {}
// swap strategy without changing any other code
setPricing(strategy: PricingStrategy) {
this.pricing = strategy;
}
quote(basePrice: number, quantity: number): number {
return this.pricing.calculate(basePrice, quantity);
}
}
// usage — swap at runtime based on customer type
const checkout = new CheckoutService(new StandardPricing());
if (customer.isPremium) checkout.setPricing(new PremiumPricing());
if (order.quantity > 50) checkout.setPricing(new BulkPricing());
const total = checkout.quote(29.99, order.quantity);Why it matters
Long if/else chains conditioned on type or mode are a classic maintainability problem — every new variant requires touching existing code, risking regression. Strategy gives each variant its own isolated, testable unit.
✓ When to use
- →Multiple algorithms for the same task (sort, price, ship, compress, validate)
- →Algorithm selection needs to happen at runtime based on context
- →When you want to eliminate conditionals that grow with each new variant
- →When variants need to be independently testable
✗ When NOT to use
- →Only one algorithm exists and no variation is anticipated
- →The number of strategies is small, fixed, and never changes — a simple conditional is clearer
Trade-offs
Adding a new variant requires no changes to existing code
Small number of strategies can over-engineer what a simple conditional handles fine
Each strategy is independently testable in isolation
Clients must know which strategies exist to select one
Open/Closed Principle — context is open for extension via new strategies
Strategy proliferation: many small single-method classes can be hard to navigate
In production
Comparator<T> is the canonical strategy interface — pass any comparison logic to sort()
Payment method handlers (card, SEPA, PayPal) implement a common charge interface
Encryption strategies (AES256, aws:kms) are selectable per-object at write time
Industry adoption
Related principles
Dependency Injection
Supply a component's dependencies from the outside rather than letting it construct them — so implementations can be swapped without changing the component.
Observer Pattern
Let a subject notify a dynamic list of dependents automatically when its state changes — without the subject knowing who is listening.
Clean Architecture
LiveOrganise code into concentric dependency rings so business logic never depends on frameworks, databases, or UI.