Back to Blog
design-patternscsharptypescriptbehavioral-patterns

Strategy Pattern — FixIt Pro Series #20

Fixed Rate, Hourly Rate, Emergency Surcharge, Contract Rate, with swappable pricing algorithms chosen at runtime without a single conditional. Learn the Strategy pattern in C# and TypeScript.

Series: Design Patterns with FixIt Pro  ·  Episode 20 / 22  ·  Behavioral Pattern
Previous: #19 — State Pattern


The Scenario

FixIt Pro charges for jobs in four different ways depending on the client and the situation:

  • Fixed Rate — a flat quoted price agreed upfront, regardless of time taken
  • Hourly Rate — a per-hour charge with a minimum callout fee
  • Emergency Surcharge — hourly rate plus a percentage surcharge for after-hours work
  • Contract Rate — a discounted rate for clients on an annual service contract

The naive implementation puts all four inside the billing service:

public decimal CalculateCost(JobCard job, string pricingType)
{
    if (pricingType == "fixed")
        return job.QuotedPrice;
    else if (pricingType == "hourly")
        return Math.Max(job.HoursWorked * 450m, 350m);
    else if (pricingType == "emergency")
        return Math.Max(job.HoursWorked * 450m, 350m) * 1.5m;
    else if (pricingType == "contract")
        return job.HoursWorked * 450m * 0.85m;
    else
        throw new ArgumentException("Unknown pricing type");
}

Four pricing rules in one method. Add weekend rates, public holiday rates, or a first-time client discount and the method keeps growing. Change the base hourly rate and you edit it in three places. Test one pricing rule and you have to instantiate the whole billing service.

The Strategy pattern extracts each algorithm into its own class.


What Is the Strategy Pattern?

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

The context holds a reference to a strategy object and delegates the work to it. Swapping the strategy swaps the behaviour, at runtime, with no conditionals in the context.

The three participants

Role FixIt Pro equivalent
Strategy interface IPricingStrategy — declares Calculate(job)
Concrete Strategies FixedRateStrategy, HourlyRateStrategy, EmergencyRateStrategy, ContractRateStrategy
Context BillingService — holds a strategy, delegates calculation to it

C# Implementation

// ── Model ──────────────────────────────────────────────────

public class JobCard
{
    public string  JobId        { get; init; } = Guid.NewGuid().ToString()[..8];
    public string  Title        { get; init; } = string.Empty;
    public string  Category     { get; init; } = string.Empty;
    public decimal QuotedPrice  { get; init; }
    public decimal HoursWorked  { get; set;  }
    public bool    IsAfterHours { get; set;  }

    public override string ToString() =>
        $"Job #{JobId} '{Title}' [{Category}] — {HoursWorked}h";
}

// ── Strategy Interface ─────────────────────────────────────

public interface IPricingStrategy
{
    string  Name { get; }
    decimal Calculate(JobCard job);
    string  Explain(JobCard job);
}

// ── Concrete Strategies ────────────────────────────────────

public class FixedRateStrategy : IPricingStrategy
{
    public string Name => "Fixed Rate";

    public decimal Calculate(JobCard job) => job.QuotedPrice;

    public string Explain(JobCard job) =>
        $"Flat quoted price of N${job.QuotedPrice:N2}, regardless of time taken.";
}

public class HourlyRateStrategy : IPricingStrategy
{
    private readonly decimal _hourlyRate;
    private readonly decimal _minimumCallout;

    public HourlyRateStrategy(decimal hourlyRate = 450m, decimal minimumCallout = 350m)
    {
        _hourlyRate     = hourlyRate;
        _minimumCallout = minimumCallout;
    }

    public string Name => "Hourly Rate";

    public decimal Calculate(JobCard job) =>
        Math.Max(job.HoursWorked * _hourlyRate, _minimumCallout);

    public string Explain(JobCard job)
    {
        var raw = job.HoursWorked * _hourlyRate;
        return raw < _minimumCallout
            ? $"{job.HoursWorked}h × N${_hourlyRate:N2} = N${raw:N2}, " +
              $"below minimum callout — charged N${_minimumCallout:N2}."
            : $"{job.HoursWorked}h × N${_hourlyRate:N2} = N${raw:N2}.";
    }
}

public class EmergencyRateStrategy : IPricingStrategy
{
    private readonly IPricingStrategy _baseStrategy;
    private readonly decimal          _surchargeRate;

    public EmergencyRateStrategy(
        IPricingStrategy baseStrategy,
        decimal          surchargeRate = 0.5m)
    {
        _baseStrategy  = baseStrategy;
        _surchargeRate = surchargeRate;
    }

    public string Name => $"Emergency ({_surchargeRate:P0} surcharge)";

    public decimal Calculate(JobCard job) =>
        _baseStrategy.Calculate(job) * (1 + _surchargeRate);

    public string Explain(JobCard job)
    {
        var baseAmount = _baseStrategy.Calculate(job);
        var surcharge  = baseAmount * _surchargeRate;

        return $"{_baseStrategy.Explain(job)} " +
               $"After-hours surcharge of {_surchargeRate:P0} " +
               $"(N${surcharge:N2}) applied.";
    }
}

public class ContractRateStrategy : IPricingStrategy
{
    private readonly IPricingStrategy _baseStrategy;
    private readonly decimal          _discountRate;
    private readonly string           _contractRef;

    public ContractRateStrategy(
        IPricingStrategy baseStrategy,
        string           contractRef,
        decimal          discountRate = 0.15m)
    {
        _baseStrategy = baseStrategy;
        _contractRef  = contractRef;
        _discountRate = discountRate;
    }

    public string Name => $"Contract Rate ({_discountRate:P0} discount)";

    public decimal Calculate(JobCard job) =>
        _baseStrategy.Calculate(job) * (1 - _discountRate);

    public string Explain(JobCard job)
    {
        var baseAmount = _baseStrategy.Calculate(job);
        var discount   = baseAmount * _discountRate;

        return $"{_baseStrategy.Explain(job)} " +
               $"Contract {_contractRef} discount of {_discountRate:P0} " +
               $"(N${discount:N2}) applied.";
    }
}

// ── Context — BillingService ───────────────────────────────

public class BillingService
{
    private IPricingStrategy _strategy;

    public BillingService(IPricingStrategy strategy)
    {
        _strategy = strategy;
    }

    // Strategy can be swapped at runtime
    public void SetStrategy(IPricingStrategy strategy)
    {
        Console.WriteLine($"  [Billing] Pricing strategy set to: {strategy.Name}");
        _strategy = strategy;
    }

    public decimal GenerateInvoice(JobCard job)
    {
        var amount = _strategy.Calculate(job);

        Console.WriteLine($"\n  Invoice for {job}");
        Console.WriteLine($"    Strategy : {_strategy.Name}");
        Console.WriteLine($"    Basis    : {_strategy.Explain(job)}");
        Console.WriteLine($"    TOTAL    : N${amount:N2}");

        return amount;
    }
}

// ── Client Code ────────────────────────────────────────────

class Program
{
    static void Main()
    {
        var job = new JobCard
        {
            Title       = "Replace burst pipe under sink",
            Category    = "Plumbing",
            QuotedPrice = 1_200m,
            HoursWorked = 3.5m
        };

        // Start with fixed rate
        var billing = new BillingService(new FixedRateStrategy());
        billing.GenerateInvoice(job);

        // Switch to hourly
        billing.SetStrategy(new HourlyRateStrategy());
        billing.GenerateInvoice(job);

        // Switch to emergency — composes hourly with a surcharge
        billing.SetStrategy(new EmergencyRateStrategy(new HourlyRateStrategy()));
        billing.GenerateInvoice(job);

        // Switch to contract rate — composes hourly with a discount
        billing.SetStrategy(
            new ContractRateStrategy(new HourlyRateStrategy(), "RFL-2025-014"));
        billing.GenerateInvoice(job);

        // Short job — demonstrates minimum callout
        var shortJob = new JobCard
        {
            Title       = "Tighten leaking tap",
            Category    = "Plumbing",
            HoursWorked = 0.5m
        };

        billing.SetStrategy(new HourlyRateStrategy());
        billing.GenerateInvoice(shortJob);
    }
}

Output:

  Invoice for Job #f3a12b 'Replace burst pipe under sink' [Plumbing] — 3.5h
    Strategy : Fixed Rate
    Basis    : Flat quoted price of N$1,200.00, regardless of time taken.
    TOTAL    : N$1,200.00

  [Billing] Pricing strategy set to: Hourly Rate

  Invoice for Job #f3a12b 'Replace burst pipe under sink' [Plumbing] — 3.5h
    Strategy : Hourly Rate
    Basis    : 3.5h × N$450.00 = N$1,575.00.
    TOTAL    : N$1,575.00

  [Billing] Pricing strategy set to: Emergency (50% surcharge)

  Invoice for Job #f3a12b 'Replace burst pipe under sink' [Plumbing] — 3.5h
    Strategy : Emergency (50% surcharge)
    Basis    : 3.5h × N$450.00 = N$1,575.00. After-hours surcharge of 50% (N$787.50) applied.
    TOTAL    : N$2,362.50

  [Billing] Pricing strategy set to: Contract Rate (15% discount)

  Invoice for Job #f3a12b 'Replace burst pipe under sink' [Plumbing] — 3.5h
    Strategy : Contract Rate (15% discount)
    Basis    : 3.5h × N$450.00 = N$1,575.00. Contract RFL-2025-014 discount of 15% (N$236.25) applied.
    TOTAL    : N$1,338.75

  [Billing] Pricing strategy set to: Hourly Rate

  Invoice for Job #a7d45e 'Tighten leaking tap' [Plumbing] — 0.5h
    Strategy : Hourly Rate
    Basis    : 0.5h × N$450.00 = N$225.00, below minimum callout — charged N$350.00.
    TOTAL    : N$350.00

TypeScript Implementation

// ── Model/JobCard.ts ───────────────────────────────────────

const shortId = () => Math.random().toString(36).slice(2, 10);

export class JobCard {
  readonly jobId: string = shortId();
  hoursWorked:    number = 0;
  isAfterHours:   boolean = false;

  constructor(
    readonly title:       string,
    readonly category:    string,
    readonly quotedPrice: number = 0
  ) {}

  toString(): string {
    return `Job #${this.jobId} '${this.title}' [${this.category}] — ${this.hoursWorked}h`;
  }
}

// ── Strategy/IPricingStrategy.ts ───────────────────────────

import { JobCard } from "../Model/JobCard";

export interface IPricingStrategy {
  readonly name: string;
  calculate(job: JobCard): number;
  explain(job: JobCard): string;
}

// ── Strategy/FixedRateStrategy.ts ──────────────────────────

import { IPricingStrategy } from "./IPricingStrategy";
import { JobCard          } from "../Model/JobCard";

export class FixedRateStrategy implements IPricingStrategy {
  readonly name = "Fixed Rate";

  calculate(job: JobCard): number { return job.quotedPrice; }

  explain(job: JobCard): string {
    return `Flat quoted price of N$${job.quotedPrice.toFixed(2)}, regardless of time taken.`;
  }
}

// ── Strategy/HourlyRateStrategy.ts ─────────────────────────

import { IPricingStrategy } from "./IPricingStrategy";
import { JobCard          } from "../Model/JobCard";

export class HourlyRateStrategy implements IPricingStrategy {
  readonly name = "Hourly Rate";

  constructor(
    private readonly hourlyRate:     number = 450,
    private readonly minimumCallout: number = 350
  ) {}

  calculate(job: JobCard): number {
    return Math.max(job.hoursWorked * this.hourlyRate, this.minimumCallout);
  }

  explain(job: JobCard): string {
    const raw = job.hoursWorked * this.hourlyRate;

    return raw < this.minimumCallout
      ? `${job.hoursWorked}h × N$${this.hourlyRate.toFixed(2)} = N$${raw.toFixed(2)}, ` +
        `below minimum callout — charged N$${this.minimumCallout.toFixed(2)}.`
      : `${job.hoursWorked}h × N$${this.hourlyRate.toFixed(2)} = N$${raw.toFixed(2)}.`;
  }
}

// ── Strategy/EmergencyRateStrategy.ts ──────────────────────

import { IPricingStrategy } from "./IPricingStrategy";
import { JobCard          } from "../Model/JobCard";

export class EmergencyRateStrategy implements IPricingStrategy {
  constructor(
    private readonly baseStrategy:  IPricingStrategy,
    private readonly surchargeRate: number = 0.5
  ) {}

  get name(): string {
    return `Emergency (${(this.surchargeRate * 100).toFixed(0)}% surcharge)`;
  }

  calculate(job: JobCard): number {
    return this.baseStrategy.calculate(job) * (1 + this.surchargeRate);
  }

  explain(job: JobCard): string {
    const baseAmount = this.baseStrategy.calculate(job);
    const surcharge  = baseAmount * this.surchargeRate;

    return `${this.baseStrategy.explain(job)} ` +
      `After-hours surcharge of ${(this.surchargeRate * 100).toFixed(0)}% ` +
      `(N$${surcharge.toFixed(2)}) applied.`;
  }
}

// ── Strategy/ContractRateStrategy.ts ───────────────────────

import { IPricingStrategy } from "./IPricingStrategy";
import { JobCard          } from "../Model/JobCard";

export class ContractRateStrategy implements IPricingStrategy {
  constructor(
    private readonly baseStrategy: IPricingStrategy,
    private readonly contractRef:  string,
    private readonly discountRate: number = 0.15
  ) {}

  get name(): string {
    return `Contract Rate (${(this.discountRate * 100).toFixed(0)}% discount)`;
  }

  calculate(job: JobCard): number {
    return this.baseStrategy.calculate(job) * (1 - this.discountRate);
  }

  explain(job: JobCard): string {
    const baseAmount = this.baseStrategy.calculate(job);
    const discount   = baseAmount * this.discountRate;

    return `${this.baseStrategy.explain(job)} ` +
      `Contract ${this.contractRef} discount of ` +
      `${(this.discountRate * 100).toFixed(0)}% (N$${discount.toFixed(2)}) applied.`;
  }
}

// ── Context/BillingService.ts ──────────────────────────────

import { IPricingStrategy } from "../Strategy/IPricingStrategy";
import { JobCard          } from "../Model/JobCard";

export class BillingService {
  constructor(private strategy: IPricingStrategy) {}

  setStrategy(strategy: IPricingStrategy): void {
    console.log(`  [Billing] Pricing strategy set to: ${strategy.name}`);
    this.strategy = strategy;
  }

  generateInvoice(job: JobCard): number {
    const amount = this.strategy.calculate(job);

    console.log(`\n  Invoice for ${job}`);
    console.log(`    Strategy : ${this.strategy.name}`);
    console.log(`    Basis    : ${this.strategy.explain(job)}`);
    console.log(`    TOTAL    : N$${amount.toFixed(2)}`);

    return amount;
  }
}

// ── App.ts ─────────────────────────────────────────────────

import { JobCard               } from "./Model/JobCard";
import { BillingService        } from "./Context/BillingService";
import { FixedRateStrategy     } from "./Strategy/FixedRateStrategy";
import { HourlyRateStrategy    } from "./Strategy/HourlyRateStrategy";
import { EmergencyRateStrategy } from "./Strategy/EmergencyRateStrategy";
import { ContractRateStrategy  } from "./Strategy/ContractRateStrategy";

const job = new JobCard("Replace burst pipe under sink", "Plumbing", 1_200);
job.hoursWorked = 3.5;

const billing = new BillingService(new FixedRateStrategy());
billing.generateInvoice(job);

billing.setStrategy(new HourlyRateStrategy());
billing.generateInvoice(job);

billing.setStrategy(new EmergencyRateStrategy(new HourlyRateStrategy()));
billing.generateInvoice(job);

billing.setStrategy(
  new ContractRateStrategy(new HourlyRateStrategy(), "RFL-2025-014"));
billing.generateInvoice(job);

const shortJob = new JobCard("Tighten leaking tap", "Plumbing");
shortJob.hoursWorked = 0.5;

billing.setStrategy(new HourlyRateStrategy());
billing.generateInvoice(shortJob);

C# vs TypeScript — Key Differences

Aspect C# TypeScript
Percentage formatting {_surchargeRate:P0} format specifier (rate * 100).toFixed(0) + "%" manual
Decimal type decimal — exact for currency number — use decimal.js in production
Computed property public string Name => $"..." get name(): string { return ... }
Default parameters decimal surchargeRate = 0.5m surchargeRate: number = 0.5
Strategy as delegate Func<JobCard, decimal> is a valid lightweight alternative (job: JobCard) => number type alias works too

C#'s P0 format specifier for percentages is a small but genuine convenience — {0.15:P0} renders as 15% automatically. TypeScript requires manual multiplication and formatting.


Strategy Without Classes

For simple algorithms, you don't always need a class per strategy. Both languages support function-based strategies:

C#:

public class BillingService
{
    private Func<JobCard, decimal> _pricing;

    public BillingService(Func<JobCard, decimal> pricing) => _pricing = pricing;

    public decimal GenerateInvoice(JobCard job) => _pricing(job);
}

// Usage
var billing = new BillingService(job => job.QuotedPrice);
billing = new BillingService(job => Math.Max(job.HoursWorked * 450m, 350m));

TypeScript:

type PricingStrategy = (job: JobCard) => number;

class BillingService {
  constructor(private pricing: PricingStrategy) {}
  generateInvoice(job: JobCard): number { return this.pricing(job); }
}

// Usage
const billing = new BillingService(job => job.quotedPrice);

Use classes when strategies need state, configuration, or multiple methods (like Explain()). Use functions when the strategy is a single pure calculation.


Strategy vs State — The Definitive Comparison

We covered State in Episode 19. Here's the two side by side:

State (Ep 19) Strategy (Ep 20)
Purpose Behaviour changes as internal state changes Choose one algorithm from a family
Who decides The states decide their own transitions The client picks the strategy
Do they know each other? Yes — DraftState creates AssignedState No — strategies are completely independent
Transitions Core to the pattern Not applicable
Lifetime Changes repeatedly through the object's life Usually set once, may be swapped
FixIt Pro Job lifecycle: Draft → Assigned → Closed Pricing: Fixed vs Hourly vs Emergency

The class diagrams are nearly identical. The intent is completely different. If the objects transition into each other, it's State. If the client picks one, it's Strategy.


When to Use the Strategy

Use it when:

  • You have several variants of an algorithm and need to switch between them
  • You want to isolate algorithm implementation details from the code that uses it
  • A class has a large conditional that selects between related behaviours
  • You want to test algorithms independently of their consumers

Avoid it when:

  • You only have one or two algorithms and they rarely change
  • The algorithms are trivial one-liners where a lambda or delegate is simpler
  • The client shouldn't need to know about the different strategies at all, consider a Factory or the State pattern instead

Real-World Takeaway

The Strategy is one of the most practically applied patterns in production code. In .NET, IComparer<T> passed to List.Sort() is a Strategy where you supply the comparison algorithm. Authentication schemes in ASP.NET Core are Strategies where JWT, Cookie, OAuth, each interchangeable. Compression algorithms (GZipStream, DeflateStream, BrotliStream) are Strategies behind a common Stream interface. In frontend, sorting and filtering functions passed to array methods are function-based Strategies. Payment processors, e.g. Stripe, PayPal, PayFast, are behind a common IPaymentProvider interface are Strategies.

In FixIt Pro, the billing service contains zero pricing logic. Adding a weekend rate, public holiday rate, or first-time client discount means writing one new strategy class. Nothing else changes. And because EmergencyRateStrategy and ContractRateStrategy wrap another strategy, you can compose them in an emergency contract job stacks both the surcharge and the discount without either class knowing about the other.


Repo Structure for This Episode

github.com/antonlungameni/fixit-pro-design-patterns

fixit-pro-design-patterns/
├── csharp/Behavioral/20-Strategy/
│   ├── Model/JobCard.cs
│   ├── Strategy/IPricingStrategy.cs
│   ├── Strategy/FixedRateStrategy.cs
│   ├── Strategy/HourlyRateStrategy.cs
│   ├── Strategy/EmergencyRateStrategy.cs
│   ├── Strategy/ContractRateStrategy.cs
│   ├── Context/BillingService.cs
│   └── Program.cs
└── typescript/Behavioral/20-Strategy/
    ├── Model/JobCard.ts
    ├── Strategy/IPricingStrategy.ts
    ├── Strategy/FixedRateStrategy.ts
    ├── Strategy/HourlyRateStrategy.ts
    ├── Strategy/EmergencyRateStrategy.ts
    ├── Strategy/ContractRateStrategy.ts
    ├── Context/BillingService.ts
    └── App.ts

Previous: #19 — State Pattern
Next up: #21 — Template Method Pattern A base JobInspectionReport with a fixed sequence of steps, where individual steps are overridden per job type, but the overall algorithm structure stays locked.