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.