Skip to content
Angular ng patterns 4 min read

Strategy Pattern

The strategy pattern lets you define a family of interchangeable algorithms behind a common interface and pick the right one at runtime instead of hard-coding branching logic. In Angular this maps cleanly onto dependency injection: each strategy is an injectable that implements a shared contract, and a DI token selects or resolves the implementation. The result is code free of sprawling if/switch ladders, open to new behaviors without touching existing ones.

When the pattern earns its keep

Reach for strategy whenever a single responsibility has several valid implementations chosen by configuration, user input, or context — payment processors, export formats (CSV, JSON, PDF), sorting rules, pricing tiers, or shipping calculators. The classic smell is a growing switch statement that everyone edits whenever a new case appears. Strategy replaces that branching with polymorphism: adding a behavior means adding a class, not modifying a conditional.

Defining the contract

Start with an interface every strategy must satisfy. Keep it narrow — one focused method is ideal.

export interface ShippingStrategy {
  readonly id: string;
  calculate(weightKg: number, distanceKm: number): number;
}

Each concrete strategy is a standalone injectable implementing that contract.

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class StandardShipping implements ShippingStrategy {
  readonly id = 'standard';
  calculate(weightKg: number, distanceKm: number): number {
    return 5 + weightKg * 0.5 + distanceKm * 0.02;
  }
}

@Injectable({ providedIn: 'root' })
export class ExpressShipping implements ShippingStrategy {
  readonly id = 'express';
  calculate(weightKg: number, distanceKm: number): number {
    return 15 + weightKg * 1.2 + distanceKm * 0.05;
  }
}

Registering strategies with a multi-provider token

The idiomatic Angular way to collect interchangeable implementations is a multi InjectionToken. Every strategy registers itself under the same token, and the consumer injects the whole array.

import { InjectionToken } from '@angular/core';

export const SHIPPING_STRATEGIES = new InjectionToken<ShippingStrategy[]>(
  'SHIPPING_STRATEGIES',
);

Provide them at the application level — typically in app.config.ts.

import { ApplicationConfig } from '@angular/core';
import { SHIPPING_STRATEGIES } from './shipping.token';
import { StandardShipping } from './standard-shipping';
import { ExpressShipping } from './express-shipping';

export const appConfig: ApplicationConfig = {
  providers: [
    { provide: SHIPPING_STRATEGIES, useExisting: StandardShipping, multi: true },
    { provide: SHIPPING_STRATEGIES, useExisting: ExpressShipping, multi: true },
  ],
};

Selecting a strategy at runtime

A small resolver service injects the array and looks up the right implementation by key. This is the heart of the pattern — the caller asks for a behavior by name and gets it without knowing which class fulfills the request.

import { Injectable, inject } from '@angular/core';
import { SHIPPING_STRATEGIES } from './shipping.token';

@Injectable({ providedIn: 'root' })
export class ShippingService {
  private readonly strategies = inject(SHIPPING_STRATEGIES);

  quote(method: string, weightKg: number, distanceKm: number): number {
    const strategy = this.strategies.find((s) => s.id === method);
    if (!strategy) {
      throw new Error(`Unknown shipping method: ${method}`);
    }
    return strategy.calculate(weightKg, distanceKm);
  }

  available(): string[] {
    return this.strategies.map((s) => s.id);
  }
}

Adding a PriorityShipping strategy later requires only a new class and one provider line — ShippingService never changes.

Driving it from a component

A standalone component wires the selected method to a signal and recomputes the quote reactively.

import { Component, computed, inject, signal } from '@angular/core';
import { ShippingService } from './shipping.service';

@Component({
  selector: 'app-checkout',
  standalone: true,
  template: `
    <select [value]="method()" (change)="method.set($any($event.target).value)">
      @for (m of methods; track m) {
        <option [value]="m">{{ m }}</option>
      }
    </select>
    <p>Cost: {{ cost() | currency }}</p>
  `,
})
export class CheckoutComponent {
  private shipping = inject(ShippingService);
  methods = this.shipping.available();
  method = signal('standard');
  cost = computed(() =>
    this.shipping.quote(this.method(), 2, 100),
  );
}

Output:

standard  → Cost: $8.00
express   → Cost: $22.40

Strategy vs. branching logic

Concernswitch statementStrategy with DI
Adding a behaviorEdit existing codeAdd a class + provider
TestabilityTest all branches togetherTest each strategy alone
CouplingCaller knows every caseCaller depends on a token
Lazy loadingHardProvide per-route or per-feature

Prefer useExisting over useClass in multi providers when the strategies are already providedIn: 'root'. This reuses the singleton instance instead of creating a second copy under the token.

Best Practices

  • Keep the strategy interface minimal — one or two methods — so new implementations are cheap to write and easy to test.
  • Register interchangeable behaviors through a single multi InjectionToken rather than a hand-maintained map.
  • Give each strategy a stable id (or use a Map keyed by an enum) so selection is explicit and type-safe.
  • Fail loudly when no strategy matches the requested key rather than silently falling back.
  • Keep strategies pure where possible; if one needs collaborators, inject them rather than reaching for global state.
  • Provide route- or feature-scoped strategies when only part of the app needs them, enabling lazy loading.
Last updated June 14, 2026
Was this helpful?