<arch.design/>
Principles/Strategy Pattern
{ }CodeArchitecturebeginner1994gofalgorithminterchangeableopen-closed

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/5
{ }
Operates at: Code level

Inside 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 layout
project structure
src/
├── 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

Java Collections

Comparator<T> is the canonical strategy interface — pass any comparison logic to sort()

Stripe

Payment method handlers (card, SEPA, PayPal) implement a common charge interface

AWS S3

Encryption strategies (AES256, aws:kms) are selectable per-object at write time

Industry adoption

4/5Widely adopted — mainstream at medium-to-large engineering orgs.

Related principles