JavaScript Strategy Pattern: Interchangeable Algorithms

The Strategy Pattern is a behavioral design pattern that allows developers to define a family of algorithms, encapsulate each one, and make them interchangeable at runtime. This article explains how the Strategy Pattern works in JavaScript, demonstrates both object-oriented and functional implementations, and illustrates how decoupling business logic from algorithm execution creates modular, scalable, and maintainable applications.

Understanding the Strategy Pattern

In software development, applications often need to execute different variants of an algorithm based on user input, system state, or business rules. Without a structured pattern, this typically results in bulky if-else or switch statements that violate the Open/Closed Principle (software entities should be open for extension, but closed for modification).

The Strategy Pattern resolves this issue by dividing the logic into three components:

  1. Context: The entity that maintains a reference to a strategy and delegates algorithmic work to it.
  2. Strategy: The common interface or signature that all algorithms adhere to.
  3. Concrete Strategies: The individual implementations of the specific algorithms.

Because JavaScript treats functions as first-class citizens, the Strategy Pattern can be implemented using standard classes or lightweight functional objects.

Implementing the Strategy Pattern in JavaScript

Consider a common scenario: calculating shipping costs across different carriers.

Object-Oriented Approach

In an object-oriented style, each shipping carrier acts as a concrete strategy class implementing a shared method name (calculate).

// Concrete Strategies
class FedExStrategy {
  calculate(packageDetails) {
    return packageDetails.weight * 2.45 + packageDetails.distance * 0.15;
  }
}

class UPSStrategy {
  calculate(packageDetails) {
    return packageDetails.weight * 2.10 + packageDetails.distance * 0.18;
  }
}

class PostalStrategy {
  calculate(packageDetails) {
    return packageDetails.weight * 1.50 + packageDetails.distance * 0.10;
  }
}

// Context
class ShippingContext {
  constructor() {
    this.strategy = null;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  calculateShipping(packageDetails) {
    if (!this.strategy) {
      throw new Error("Shipping strategy has not been set.");
    }
    return this.strategy.calculate(packageDetails);
  }
}

// Usage
const packageDetails = { weight: 10, distance: 100 };
const shipping = new ShippingContext();

shipping.setStrategy(new FedExStrategy());
console.log(`FedEx: $${shipping.calculateShipping(packageDetails)}`);

shipping.setStrategy(new UPSStrategy());
console.log(`UPS: $${shipping.calculateShipping(packageDetails)}`);

Functional Approach

JavaScript allows for a more concise functional implementation using an object map containing pure functions.

// Strategies defined as a dictionary of functions
const shippingStrategies = {
  fedex: ({ weight, distance }) => weight * 2.45 + distance * 0.15,
  ups: ({ weight, distance }) => weight * 2.10 + distance * 0.18,
  postal: ({ weight, distance }) => weight * 1.50 + distance * 0.10,
};

// Context function
function calculateShipping(carrier, packageDetails) {
  const strategy = shippingStrategies[carrier];
  if (!strategy) {
    throw new Error(`Unsupported carrier: ${carrier}`);
  }
  return strategy(packageDetails);
}

// Usage
const details = { weight: 10, distance: 100 };
console.log(calculateShipping('fedex', details));
console.log(calculateShipping('postal', details));

How Interchangeability is Achieved

The Strategy Pattern enables dynamic interchangeability through three core mechanisms:

By leveraging the Strategy Pattern, JavaScript applications can seamlessly swap calculation engines, validation routines, authentication methods, or rendering logic while keeping the codebase clean, testable, and decoupled.