low-level-design3 min read

Strategy Design Pattern Tutorial from Scratch (2026)

Strategy Design Pattern Tutorial from Scratch (2026)

Published:  |  Category: Low Level Design  |  Reading time: ~15 min
Strategy Design Pattern Tutorial from Scratch (2026)

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.

This pattern is ideal when you have multiple ways to perform an operation and want to select the appropriate algorithm at runtime without using conditional statements. It promotes composition over inheritance.

Algorithm Families and Interchangeable Behaviors

The Strategy pattern uses three participants: the Context maintains a reference to a Strategy object and delegates work to it; the Strategy interface declares the algorithm contract; and Concrete Strategies implement different versions of the algorithm.

This separation allows new algorithms to be added without modifying existing code, following the Open-Closed Principle. Clients can switch strategies at runtime by passing a different strategy object to the context.

// Strategy interface
class SortingStrategy {
  sort(data) {
    throw new Error('sort() must be implemented');
  }
}

// Concrete strategies
class BubbleSort extends SortingStrategy {
  sort(data) {
    const arr = [...data];
    for (let i = 0; i < arr.length - 1; i++) {
      for (let j = 0; j < arr.length - i - 1; j++) {
        if (arr[j] > arr[j + 1]) {
          [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        }
      }
    }
    return arr;
  }
}

class QuickSort extends SortingStrategy {
  sort(data) {
    if (data.length <= 1) return data;
    const arr = [...data];
    const pivot = arr[0];
    const left = [];
    const right = [];
    for (let i = 1; i < arr.length; i++) {
      arr[i] < pivot ? left.push(arr[i]) : right.push(arr[i]);
    }
    return [...this.sort(left), pivot, ...this.sort(right)];
  }
}

// Context
class Sorter {
  constructor(strategy) {
    this.strategy = strategy;
  }
  setStrategy(strategy) {
    this.strategy = strategy;
  }
  sort(data) {
    return this.strategy.sort(data);
  }
}

const data = [3, 1, 4, 1, 5, 9];
const sorter = new Sorter(new BubbleSort());
console.log('Bubble:', sorter.sort(data));
sorter.setStrategy(new QuickSort());
console.log('Quick:', sorter.sort(data));

Payment Processing Example with Strategy

A practical application of Strategy is payment processing in e-commerce. Different payment methods (credit card, PayPal, cryptocurrency) share the same interface (pay) but implement the logic differently. The shopping cart context uses whichever payment strategy the customer selects.

This approach eliminates cumbersome switch or if-else chains when handling multiple payment methods. Adding a new payment method simply requires implementing a new concrete strategy.

// Payment Strategy interface
class PaymentStrategy {
  pay(amount) {
    throw new Error('pay() must be implemented');
  }
}

class CreditCardPayment extends PaymentStrategy {
  constructor(cardNumber, cvv) {
    super();
    this.cardNumber = cardNumber;
    this.cvv = cvv;
  }
  pay(amount) {
    console.log(`Paid $${amount} with credit card ${this.cardNumber.slice(-4)}`);
    return { success: true, method: 'credit_card', amount };
  }
}

class PayPalPayment extends PaymentStrategy {
  constructor(email) {
    super();
    this.email = email;
  }
  pay(amount) {
    console.log(`Paid $${amount} via PayPal account ${this.email}`);
    return { success: true, method: 'paypal', amount };
  }
}

class CryptoPayment extends PaymentStrategy {
  constructor(walletAddress) {
    super();
    this.walletAddress = walletAddress;
  }
  pay(amount) {
    console.log(`Paid $${amount} in crypto to ${this.walletAddress.slice(0, 6)}...`);
    return { success: true, method: 'crypto', amount };
  }
}

// Context
class ShoppingCart {
  constructor() {
    this.items = [];
    this.paymentStrategy = null;
  }
  addItem(item) {
    this.items.push(item);
  }
  setPaymentStrategy(strategy) {
    this.paymentStrategy = strategy;
  }
  checkout() {
    const total = this.items.reduce((sum, item) => sum + item.price, 0);
    return this.paymentStrategy.pay(total);
  }
}

const cart = new ShoppingCart();
cart.addItem({ name: 'Book', price: 20 });
cart.addItem({ name: 'Pen', price: 5 });
cart.setPaymentStrategy(new PayPalPayment('user@example.com'));
cart.checkout();

Frequently Asked Questions

When should I use the Strategy pattern instead of conditionals?

Use Strategy when you have multiple algorithms that vary independently and are selected at runtime. If you have fewer than three variants that rarely change, simple conditionals may be more practical. Strategy shines when algorithms are complex or new variants are added frequently.

What is the difference between Strategy and State patterns?

Strategy lets the client select an algorithm from a family of interchangeable options. State allows an object to alter its behavior when its internal state changes, with state transitions often managed by the context. In Strategy, the client chooses; in State, the object transitions automatically.

Does Strategy work well with functional programming?

Yes, Strategy maps naturally to first-class functions. Languages with higher-order functions can implement Strategy by passing functions directly instead of defining strategy classes. JavaScript, Python, and Kotlin all support this more concise approach.

Can strategies share data or state?

Strategies should ideally be stateless from the context's perspective. If strategies need shared state, pass it as parameters or use context getters. Avoid storing mutable state in strategy objects to prevent unexpected interactions when switching strategies.

Originally published on Ayodhyyya. Last updated June 1, 2026.