← Open in the full interactive course (progress tracking, search & more)

You've been doing this since Foundations 028. Today it gets a name.

Foundations lesson 028 taught you to favor composition over inheritance by injecting an ILogger instead of inheriting from a logger base class. Intermediate 081 taught you Dependency Inversion by injecting an IPaymentGateway instead of hard-coding Stripe. Intermediate's higher-order function lessons showed you passing a Func<T, TResult> as a parameter so a method's behavior could vary without rewriting the method. All three of those were, in a very real sense, the same idea wearing different clothes.

That idea has a name: the Strategy Pattern. It's the Behavioral pattern — one of the three GoF categories from the design patterns lesson earlier in this Part — for making an algorithm swappable behind a common interface, so the code that uses the algorithm never has to know or care which specific version it's running.

In this lesson, you'll learn the formal GoF shape of Strategy, see it side by side with the lightweight Func<T>-parameter version you already know, and get honest guidance on which one to reach for.

What Is It?

The Simple Explanation

The Strategy Pattern encapsulates a family of interchangeable algorithms behind a common interface, and lets the algorithm actually used be chosen — and swapped — independently of the code that uses it. The class doing the work (the "context") holds a reference to a strategy and calls it, without knowing which concrete strategy it's holding.

The Technical Definition

Define a family of algorithms, encapsulate each one behind a shared interface, and make them interchangeable. Strategy lets the algorithm vary independently from the clients that use it — a direct application of depending on an abstraction rather than a concrete implementation, applied specifically to behavior rather than to infrastructure like a database or payment provider.

You already know this pattern

Composition over inheritance (028): inject the behavior instead of subclassing to get it. Dependency Inversion (081): the context depends on an abstraction, not a concrete class. Passing a Func<Order, decimal> as a parameter (099, 104): a single-method strategy, expressed as a delegate instead of an interface. All three ARE Strategy — this lesson just gives the shape a name and shows its classic textbook form.

Why Does It Exist? — The Problem It Solves

Without Strategy, varying behavior tends to end up as a growing conditional buried inside one method:

//  WITHOUT Strategy — every new discount rule means editing this method
public decimal CalculateDiscount(Order order, string customerTier)
{
    if (customerTier == "gold") return order.Total * 0.20m;
    if (customerTier == "silver") return order.Total * 0.10m;
    if (customerTier == "new-customer" && order.Total > 100) return 15m;
    // ...growing forever, mixing unrelated discount rules into one method,
    // violating Open/Closed — you can't add a rule without editing existing code
    return 0m;
}

This couples every discount rule's logic into one method, makes each rule impossible to test in isolation, and means adding a new discount type requires editing code that already works — risking regressions in rules that had nothing to do with the change.

PROBLEM → NEED → SOLUTION
PROBLEM
NEED
SOLUTION

Big Picture — Two Forms, Same Idea

Strategy has a classic, heavyweight GoF form, and a lightweight, idiomatic modern-C# form. Both solve exactly the same problem — they differ only in ceremony.

Classic GoF form — an interface

public interface IDiscountStrategy
{
    decimal Calculate(Order order);
}

public sealed class GoldTierDiscount : IDiscountStrategy
{
    public decimal Calculate(Order order) =>
        order.Total * 0.20m;
}

public sealed class OrderProcessor(IDiscountStrategy discount)
{
    public decimal GetFinalTotal(Order order) =>
        order.Total - discount.Calculate(order);
}

Modern lightweight form — a delegate

public sealed class OrderProcessor(
    Func<Order, decimal> calculateDiscount)
{
    public decimal GetFinalTotal(Order order) =>
        order.Total - calculateDiscount(order);
}

// No interface, no class per strategy — just a function:
var processor = new OrderProcessor(
    order => order.Total * 0.20m);

Notice: both versions let OrderProcessor vary its discount logic without changing OrderProcessor itself. The interface version needs a whole class per strategy; the delegate version needs only a lambda. Mechanically, they're the same pattern — IDiscountStrategy.Calculate and Func<Order, decimal> are both, structurally, "a single swappable operation with one input and one output."

How It Works

APPLYING STRATEGY — STEP BY STEP
1. IDENTIFY THE VARYING BEHAVIOR
2. EXTRACT A COMMON SIGNATURE
3. MOVE EACH VARIANT INTO ITS OWN UNIT
4. INJECT THE CHOSEN STRATEGY INTO THE CONTEXT

Simple Example

public interface IDiscountStrategy
{
    decimal Calculate(Order order);
}

public sealed class GoldTierDiscount : IDiscountStrategy
{
    public decimal Calculate(Order order) => order.Total * 0.20m;
}

public sealed class SilverTierDiscount : IDiscountStrategy
{
    public decimal Calculate(Order order) => order.Total * 0.10m;
}

public sealed class NoDiscount : IDiscountStrategy
{
    public decimal Calculate(Order order) => 0m;
}

public sealed class OrderProcessor(IDiscountStrategy discount)
{
    public decimal GetFinalTotal(Order order) => order.Total - discount.Calculate(order);
}

// ── Usage ──
var goldProcessor = new OrderProcessor(new GoldTierDiscount());
var total = goldProcessor.GetFinalTotal(order);   // order.Total minus 20%

Code → Meaning → Result: OrderProcessor never mentions gold, silver, or "no discount" — it only knows IDiscountStrategy. Swap the strategy instance passed to the constructor, and the discount calculation changes completely, with zero changes to OrderProcessor itself.

Real-World Example — Shipping Cost Calculation, Both Ways

A shipping cost calculator needs different pricing algorithms per carrier. Here's the same real scenario, once as a full interface hierarchy, once as a delegate — so you can compare the tradeoffs directly:

// ── Full interface form — earns its cost because carriers have REAL configuration/state ──
public interface IShippingStrategy
{
    decimal CalculateCost(Shipment shipment);
    string CarrierName { get; }
}

public sealed class FedExStrategy(FedExRateTable rates) : IShippingStrategy
{
    public string CarrierName => "FedEx";
    public decimal CalculateCost(Shipment shipment) =>
        rates.LookupRate(shipment.Weight, shipment.Destination);
}

public sealed class UpsStrategy(UpsAccountSettings account) : IShippingStrategy
{
    public string CarrierName => "UPS";
    public decimal CalculateCost(Shipment shipment) =>
        account.NegotiatedRate * shipment.Weight;
}

public sealed class ShippingCalculator(IShippingStrategy strategy)
{
    public ShippingQuote Quote(Shipment shipment) =>
        new(strategy.CarrierName, strategy.CalculateCost(shipment));
}
// Registered per-carrier in DI, resolved by name — a natural fit for a
// container-friendly, stateful, multi-method strategy.
services.AddKeyedScoped<IShippingStrategy, FedExStrategy>("fedex");
services.AddKeyedScoped<IShippingStrategy, UpsStrategy>("ups");

// ── Lightweight delegate form — fine for a simple, stateless, one-off rule ──
public sealed class FlatRateCalculator(Func<Shipment, decimal> costFormula)
{
    public decimal Quote(Shipment shipment) => costFormula(shipment);
}

var domesticFlatRate = new FlatRateCalculator(s => s.Weight <= 5 ? 4.99m : 9.99m);

The interface version earns its extra ceremony here because each carrier strategy has real constructor dependencies (a rate table, account settings), a second member (CarrierName), and needs to be discoverable and swappable through the DI container by a key. The delegate version is genuinely simpler for the flat-rate case — no state, one operation, no reason to force it into a class.

Analogy — Choosing a Route

The GPS and the driving algorithm

A GPS app doesn't hard-code "always take the highway." It lets you pick a strategy — "fastest route," "avoid tolls," "shortest distance" — and plugs whichever one you chose into the same trip-planning logic. The trip-planning code (the context) doesn't know or care how the route is calculated; it just calls "calculate route" and trusts whichever strategy is currently selected. Swap strategies mid-trip, and the planning logic never changes — only which algorithm answers the question does.

Under the Hood — What a Func<T> Strategy Actually Is

A DELEGATE IS AN INTERFACE WITH ONE METHOD, GENERATED FOR YOU
1. Func<Order, decimal> IS STRUCTURALLY EQUIVALENT TO IDiscountStrategy
2. A LAMBDA CAPTURING VARIABLES IS A STRATEGY OBJECT WITH STATE
3. THE REAL DIFFERENCE IS DISCOVERABILITY, NOT CAPABILITY

Common Confusion

"If I'm not writing an IStrategy interface, am I not really using the pattern?"

You are. The GoF book was written before C# had first-class delegates and lambdas as convenient as they are today — in 1994 Smalltalk/C++ terms, "encapsulate an algorithm behind a common interface" necessarily meant a class hierarchy, because that was the only tool available. The intent — swappable algorithm, decoupled from the code using it — is what makes something Strategy, not the specific mechanism (interface vs. delegate) used to express it. A Func<Order, decimal> parameter is a legitimate, idiomatic Strategy implementation in modern C#.

Strategy vs. Factory — don't mix these up

Factory (lesson 240) decides which object to construct. Strategy decides which algorithm to run, once you already have the object. They often appear together — a factory might construct the right strategy — but they answer different questions: "what do I build?" vs. "what do I do?"

Common Mistakes

Mistake 1 — Building a full interface hierarchy for a single, stateless, one-line rule

Writing IDiscountStrategy, three implementing classes, and a factory to select between them, for a rule that's genuinely just order.Total * 0.1m with no state and no plausible reuse elsewhere. A Func<Order, decimal> parameter says the same thing in one line, with no extra files.

Mistake 2 — Forcing a stateful, multi-method strategy into a delegate

Passing three separate Func<T> parameters into a constructor because the "strategy" really needs to expose CalculateCost, CarrierName, and EstimateDeliveryDate together, consistently. When a strategy genuinely has more than one related operation, or holds real state (a rate table, an API client), an interface keeps those pieces cohesive — as the shipping example showed.

Mistake 3 — Reintroducing the conditional the pattern was meant to remove, just to pick a strategy

IDiscountStrategy strategy = tier switch { "gold" => new GoldTierDiscount(), ... }; scattered at every call site — this is the exact same coupling problem Strategy was meant to solve, just one level removed. Centralize that selection logic in one place — a factory (lesson 240) or a DI registration — not repeated at every point a strategy is needed.

When Should I Use It?

Use the full interface version when

Use a plain Func<T> delegate when

Rule of thumb: start with Func<T>. Promote it to a full interface the moment the strategy grows a second method, needs constructor dependencies, or needs to be resolved by the DI container rather than passed directly.

Mental Model

Strategy = a swappable algorithm behind a common shape, injected into whatever needs to run it.
Interface form = for strategies with state, multiple methods, or DI registration.
Func<T> form = for one small, stateless operation — same idea, no ceremony.

Remember:
· You've been using this since 028 (composition), 081 (DIP), and 099/104 (Func<T>) — Strategy is the formal name for what those lessons were already teaching.
· A delegate is, mechanically, a one-method interface generated for you.
· Don't reintroduce the conditional Strategy was meant to eliminate at the point where you pick which strategy to use.

Key Takeaway


Check Your Understanding

You've seen Strategy in its classic and modern forms, and where you've already been using it. Let's check your understanding.

1. Which earlier lesson's core idea is most directly the same pattern as Strategy?

Show answer

Correct: A

Why A is correct: Composition over inheritance — injecting a behavior object rather than baking it into a subclass — is structurally the same move Strategy makes: the behavior is supplied from outside as an interchangeable, swappable component.

Why B is incorrect: required is about enforcing that a value is set at construction time — unrelated to swapping algorithms.

Why C is incorrect: Connection pooling is a performance/infrastructure concern, not an interchangeable-algorithm concern.

Why D is incorrect: Profiling is a diagnostic technique, unrelated to Strategy's shape.

Reinforcement: Strategy is the formal name for "inject the behavior instead of hard-coding it" — exactly what composition over inheritance already taught.

2. A ShippingCalculator needs a carrier strategy that holds a rate table, an API client, and exposes both CalculateCost and EstimateDeliveryDate. Which form fits best?

Show answer

Correct: B

Why B is correct: Real state (rate table, API client) and multiple related methods are exactly the conditions the lesson names for when the full interface form earns its complexity over a plain delegate.

Why A is incorrect: A single Func<T> can only represent one operation — it can't hold a rate table as state or expose a second method like EstimateDeliveryDate.

Why C is incorrect: A switch statement reintroduces the exact coupling and Open/Closed violation Strategy exists to remove.

Why D is incorrect: C# doesn't support multiple inheritance of classes, and an interface-based Strategy is the idiomatic, testable choice here, not an inheritance hierarchy.

Reinforcement: Promote to the full interface form once a strategy needs state or more than one method.

3. Why is a Func<Order, decimal> parameter considered a legitimate implementation of the Strategy pattern, not just "a shortcut that skips the real pattern"?

Show answer

Correct: B

Why B is correct: The "Under the Hood" section showed that a delegate is structurally equivalent to a one-method interface. GoF's 1994 book predates convenient C# lambdas — the pattern's intent (swappable algorithm, decoupled from its caller) is satisfied either way.

Why A is incorrect: Performance isn't the basis for this distinction; both compile to similar underlying mechanics.

Why C is incorrect: The GoF catalog predates C# entirely and describes the pattern in Smalltalk/C++ terms, using interfaces — it doesn't mention Func<T> at all.

Why D is incorrect: This directly contradicts the lesson's point — the delegate form is a legitimate, idiomatic modern implementation, not a lesser substitute.

Reinforcement: A pattern is defined by its intent and shape, not by the specific C# syntax used to express it.

4. What's the key difference between the Factory pattern (lesson 240) and the Strategy pattern?

Show answer

Correct: A

Why A is correct: This is exactly the distinction drawn in Common Confusion — Factory answers "what do I build?" while Strategy answers "what do I do?" — different questions, though they can be combined.

Why B is incorrect: They solve different problems and belong to different GoF categories entirely.

Why C is incorrect: This is backwards — Factory is Creational, Strategy is Behavioral.

Why D is incorrect: Neither pattern is restricted to static methods; both are commonly implemented with instance classes or delegates.

Reinforcement: Keep "what to build" (Factory) and "what to do" (Strategy) as separate questions, even though a factory can be used to construct a strategy.

You now have a name for something you've been doing since Foundations — and know exactly when to reach for a full interface versus a plain delegate.


dotnetmadeeasy.com — Learn C# and .NET, the right way.