Strategy

Advanced · 10 min read · ▶ live playground · ✦ checkpoint

A checkout starts with economy and express shipping. Then the business adds store pickup, same-day delivery, and a carrier promotion; one switch becomes the file everyone edits, and one missed branch charges a customer the wrong price.

The Strategy pattern defines a family of interchangeable algorithms behind one interface and lets a client choose one at runtime. It removes the growing branch from the order, but it introduces extra abstractions and makes configuration responsible for selecting the right algorithm.

Before: the order owns every shipping rule

The first version keeps all prices close together. That feels useful until every new shipping option changes a class that already works:

type ShippingType = "economy" | "express";
 
interface Shipment {
  weightKg: number;
}
 
class Order {
  public shippingCost(type: ShippingType, shipment: Shipment): number {
    switch (type) {
      case "economy":
        return 5 + shipment.weightKg;
      case "express":
        return 12 + shipment.weightKg * 2;
    }
  }
}

Order now has two reasons to change: the checkout flow can change, and any carrier formula can change. Adding "same-day" means extending ShippingType, adding another branch, and retesting this method. The branch violates the Open/Closed Principle because new behavior requires editing the existing selector and calculation.

Moving the same switch into a helper changes its address, not the design. The order still knows every option, and the helper still grows for every algorithm.

After: delegate through one algorithm contract

A strategy is one interchangeable algorithm that fulfills a shared contract. The context is the object that holds a strategy and delegates work to it; here, Order is the context.

Order knows how to ask for a shipping cost, but not how economy or express shipping calculates one:

class diagram

Order delegates shipping cost to the active strategy

Order delegates shipping cost to the active strategy
  1. 1ShippingStrategy is the one calculation contract every shipping algorithm fulfills.
  2. 2EconomyShipping implements the contract with its slower, lower-cost formula.
  3. 3ExpressShipping is interchangeable because it implements the same operation.
  4. 4Order holds one active strategy and delegates shippingCost() without naming a concrete option.

The composition edge is the source of runtime choice. Order holds a ShippingStrategy, while each concrete class points to the interface with a separate implements edge. There is no duplicate relationship between the same two nodes.

Here is the direct TypeScript reading:

interface Shipment {
  weightKg: number;
}
 
interface ShippingStrategy {
  calculate(shipment: Shipment): number;
}
 
class EconomyShipping implements ShippingStrategy {
  public calculate(shipment: Shipment): number {
    return 5 + shipment.weightKg;
  }
}
 
class ExpressShipping implements ShippingStrategy {
  public calculate(shipment: Shipment): number {
    return 12 + shipment.weightKg * 2;
  }
}
 
class Order {
  private shipping: ShippingStrategy;
 
  public constructor(
    private shipment: Shipment,
    shipping: ShippingStrategy,
  ) {
    this.shipping = shipping;
  }
 
  public setShipping(strategy: ShippingStrategy): void {
    this.shipping = strategy;
  }
 
  public shippingCost(): number {
    return this.shipping.calculate(this.shipment);
  }
}
 
const order = new Order({ weightKg: 3 }, new EconomyShipping());
console.log(`Economy: $${order.shippingCost().toFixed(2)}`);
 
order.setShipping(new ExpressShipping());
console.log(`Express: $${order.shippingCost().toFixed(2)}`);

Economy: $8.00 Express: $18.00

The shipment does not change between the two calls. Only the active strategy changes, so the same context produces a different cost. A future StorePickup can implement ShippingStrategy without changing Order or either existing calculation.

Selection still has to happen somewhere. A checkout controller, dependency-injection setup, or configuration map can translate the customer's choice into a strategy object. Strategy removes algorithm branches from the context; it does not make the choice disappear.

Strategy and State can share a class diagram

Both patterns often show a context holding an interface with several implementations. The difference is who controls change and why:

  • Strategy represents interchangeable behavior. A client or configuration chooses one active algorithm, and that algorithm does not normally choose its successor.
  • State represents behavior tied to an object's internal condition. State objects trigger transitions as the context moves through a lifecycle.

Changing an order from economy to express because a customer selects a radio button is Strategy. A vending machine moving from NoCoinState to HasCoinState after insertCoin() is State. You will build that transition model later in this module.

When NOT to use it

If an algorithm never varies, a strategy interface and concrete class add names, files, construction, and tests without buying a choice. A plain function is clearer for one fixed calculation.

TypeScript also makes functions first-class values, which means a function can be stored, passed, and called like other data. A lambda is an unnamed function written inline, and it is often a lighter strategy when each option is one short, stateless calculation:

interface ShippingInput {
  weightKg: number;
}
 
type ShippingFn = (shipment: ShippingInput) => number;
 
const economy: ShippingFn = (shipment) => 5 + shipment.weightKg;
const express: ShippingFn = (shipment) => 12 + shipment.weightKg * 2;

Reach for classes when strategies need named dependencies, several related operations, or their own lifecycle. Even then, callers must understand enough about the options to choose correctly. Ten tiny strategy classes can replace one visible switch with a construction graph that is harder to trace.

The trade-off is extension against navigation: each new algorithm becomes local, while understanding the available choices requires moving across more types. Use Strategy when the family really varies, selection happens at runtime, and keeping algorithms independent matters.

Checkpoint

Answer all three to mark this lesson complete

Strategy gives one context a replaceable behavior. Next, Observer lets one changing object notify many dependents without naming each one.

+50 XP on completion