Course Content
Course Overview
What are the course structure and learning goals. Why design patterns matter in real-world C#/.NET development? How to choose the right pattern in a scenario? How's the course structured and what are your learning goals?
0/4
Phase 1 – Foundations of Behavior
These patterns are intuitive, practical, and extremely common in .NET.
0/5
Phase 2 – Structural Thinking
Now we think about object relationships and architecture.
0/5
Phase 3 – Object Interaction & Responsibility Flow
We increase the abstraction level by introducing interaction between object and the flow of responsibilities.
0/5
Phase 4 – Object Creation Mastery
Creation patterns are conceptually harder for beginners because they require understanding dependency inversion.
0/4
Phase 5 – Advanced / Specialized Patterns
These require strong abstraction maturity.
0/4
Bonus – Other Useful Patterns
These are some of the patterns you may need or encounter occasionally out there in the wild.
0/5
C#/.NET Design Patterns: The Complete Guide
Pattern: Strategy Type: Behavioral Importance: ⭐⭐⭐⭐⭐ Usage Frequency: Very High

By the end of this lesson you can swap the algorithm an object runs, at runtime, without editing that object. We build a payment processor that handles three payment methods and accepts a fourth without a single edit. We write one interface and four small classes, where a conditional chain would keep growing with every requirement.

The Problem: Behavior Keeps Growing

Let’s start with an e-commerce checkout that takes one payment method, the credit card. A single if statement handles it, and for one requirement that is honest code.

Then more requirements arrive. We also need PayPal, crypto, a bank transfer and Apple Pay, and each carries its own validation and its own external service.

Read this section; don’t type it. What follows is diagnosis. We are deliberately not going to write it, and none of it belongs in your project. Our building starts under Refactoring the Example.

The tempting move is one more branch per payment method:

// Diagnosis only - we are NOT adding this to the project.
public class PaymentProcessor
{
    public void ProcessPayment(string paymentType)
    {
        if (paymentType == "CreditCard")
        {
            Console.WriteLine("Processing credit card...");
            // Validate card number, expiration, CVV
            // Connect to card network gateway
            // Handle 3D Secure authentication
        }
        else if (paymentType == "PayPal")
        {
            Console.WriteLine("Processing PayPal...");
            // Redirect to PayPal OAuth
            // Verify PayPal transaction ID
        }
        else if (paymentType == "Crypto")
        {
            Console.WriteLine("Processing crypto...");
            // Generate wallet address
            // Monitor blockchain for confirmation
        }
        else if (paymentType == "BankTransfer")
        {
            // ...
        }
        else if (paymentType == "ApplePay")
        {
            // ...
        }
    }
}

Why it rots: every method we add reopens the one file that already works. Our method grows with no ceiling, and our class owns five unrelated integrations at once. Two of us adding two payment methods collide in the same lines. To test PayPal we have to build a processor that also knows about blockchains. We will call this shape a conditional chain for the rest of the lesson.

The branch count is what turns honest code into a problem. One branch is fine. Five branches that each grow their own subroutine are one class doing five jobs.

What we want instead is to hand our processor an algorithm and let it run, with no test of which algorithm it received.

What the Strategy Pattern Is

The Strategy pattern defines a family of algorithms, puts each one in its own class, and makes them interchangeable behind a shared interface.

That sentence carries the whole pattern. Each algorithm becomes a class, the classes share an interface, and the object that needs the work holds the interface. We call that holder the context.

Strategy is a behavioral pattern: we change what an object does by handing it a collaborator, rather than by editing its code.

Structure Overview

We get three participants, one job each:

  • Strategy interface – the contract every algorithm implements.
  • Concrete Strategies – one class per algorithm, holding nothing else.
  • Context – holds a strategy through the interface and delegates to it.

In the diagram, look first at the arrow running from the context to the interface. The context points at the abstraction and never at a concrete class. That one arrow is why we can add an algorithm without reopening the context.

Unified Modeling Language class diagram of the Strategy pattern: a Context class holds a reference to a Strategy interface, and three concrete strategy classes implement that interface

Because our context depends on the abstraction, the high-level policy names the interface and the low-level algorithm implements it. That direction is the Dependency Inversion Principle.

Refactoring the Example

Here is that shape filled in with our own classes:

The Strategy pattern applied to payments: PaymentProcessor holds an IPaymentStrategy, with CreditCardPayment, PayPalPayment and CryptoPayment implementing that interface

Create IPaymentStrategy.cs and define the contract:

public interface IPaymentStrategy
{
    void ProcessPayment();
}

We declare one method and no data. The interface states what a payment algorithm must do, and states nothing about how any of them does it.

Create CreditCardPayment.cs, PayPalPayment.cs and CryptoPayment.cs, and add our three algorithms:

public class CreditCardPayment : IPaymentStrategy
{
    public void ProcessPayment()
    {
        Console.WriteLine("Processing credit card...");
    }
}

public class PayPalPayment : IPaymentStrategy
{
    public void ProcessPayment()
    {
        Console.WriteLine("Processing PayPal...");
    }
}

public class CryptoPayment : IPaymentStrategy
{
    public void ProcessPayment()
    {
        Console.WriteLine("Processing crypto...");
    }
}

Each class carries one branch of the old conditional and nothing more. We can open CryptoPayment without reading a line about cards.

Now we rewrite the processor. It keeps its name and its job, and it loses the conditional entirely. Create PaymentProcessor.cs and add our context:

public class PaymentProcessor
{
    private readonly IPaymentStrategy _paymentStrategy;

    public PaymentProcessor(IPaymentStrategy paymentStrategy)
    {
        _paymentStrategy = paymentStrategy;
    }

    public void Process()
    {
        _paymentStrategy.ProcessPayment();
    }
}

Here, we accept an IPaymentStrategy, we store it, and then we forward one call to it. Our class decides nothing. Whoever builds the processor has already made the decision, and the processor has no way to ask which algorithm it holds.

Usage

Now we compose them. Each processor receives its algorithm at construction:

var paypalProcessor = new PaymentProcessor(new PayPalPayment());
paypalProcessor.Process();

var creditCardProcessor = new PaymentProcessor(new CreditCardPayment());
creditCardProcessor.Process();

var cryptoProcessor = new PaymentProcessor(new CryptoPayment());
cryptoProcessor.Process();

Before you read on, predict two things. Which line of code chose the crypto algorithm, and how many if statements run inside PaymentProcessor to carry that choice out?

Output:

Processing PayPal...
Processing credit card...
Processing crypto...

Let’s walk it in order. We build a processor around PayPalPayment and call Process(), which forwards to that one object. Then we do the same around CreditCardPayment, and then around CryptoPayment.

Now our predictions. We chose the crypto algorithm on the new PaymentProcessor(new CryptoPayment()) line, out in the caller. The number of if statements inside PaymentProcessor is zero, and it stays zero after we add Apple Pay.

What Changed in the Design

Before, our processor selected behavior. It held every algorithm, and it grew by one branch per requirement.

Now our processor receives behavior. Each algorithm is a class we can read, test and replace on its own, and the processor is a handful of lines that never change again.

The deeper shift is where the decision lives. In the diagnosis version it lived inside a method, at run time, keyed on a string. Here it lives at the point of construction, in the caller, expressed as a type. A misspelled "PayPall" used to be a silent no-op that shipped. Now the compiler holds our list of legal choices.

Your Turn

Add a fourth algorithm: a bank transfer that has to remember which account it pays into. This is the case a lambda cannot cover on its own, because our strategy now carries data. Work in a scratch project, not in the lesson’s solution.

Here is the stub:

public class BankTransferPayment : IPaymentStrategy
{
    private readonly string _iban;

    public BankTransferPayment(string iban)
    {
        _iban = iban;
    }

    public void ProcessPayment()
    {
        // Print "Processing bank transfer to <iban>..." using the stored account.
    }
}
Solution: open after you have your own answer

Our body reads the field the constructor stored, so the algorithm and its data travel together:

public class BankTransferPayment : IPaymentStrategy
{
    private readonly string _iban;

    public BankTransferPayment(string iban)
    {
        _iban = iban;
    }

    public void ProcessPayment()
    {
        Console.WriteLine($"Processing bank transfer to {_iban}...");
    }
}

Then we hand it to the processor we already have:

var bankProcessor = new PaymentProcessor(new BankTransferPayment("DE89370400440532013000"));
bankProcessor.Process();

Output:

Processing bank transfer to DE89370400440532013000...

We changed nothing in PaymentProcessor and nothing in the other three algorithms. That is the property we bought. Notice where the account number entered: through the strategy’s own constructor, never through Process(). Our context stayed ignorant of it, which is what keeps the context stable.

Delegates as Lightweight Strategies

Not every strategy in C# needs a class. When the behavior is one method with no data, a delegate carries it:

public class DelegatePaymentProcessor
{
    private readonly Action _processPayment;

    public DelegatePaymentProcessor(Action processPayment)
    {
        _processPayment = processPayment;
    }

    public void Process() => _processPayment();
}

var processor = new DelegatePaymentProcessor(() => Console.WriteLine("Processing PayPal..."));
processor.Process();

We gave this class a second name on purpose. It is an alternative design rather than a replacement, and one name over two designs is how we lose a reader.

We reach for a delegate when our strategy is a single function with no data. We reach for the full pattern when strategies hold data, expose more than one method, or have to be resolved from configuration. BankTransferPayment above is the first of those.

When to Use Strategy

We reach for Strategy when a class holds a conditional that picks between behaviors, and we expect that list to grow. We reach for it when several algorithms serve one goal by different routes, and when the choice belongs to the caller rather than to the class doing the work.

We leave it alone when the variation is two branches that will stay two branches, since one if reads better than an interface plus two files. We leave it alone when the behavior is a single stateless function, because a delegate says the same thing in one line. And we abandon it the moment our context starts asking which strategy it holds. A context that type-checks its own strategy has quietly taken the decision back.

Strategy in the .NET Ecosystem

The clearest example in the base class library is IComparer<T>. It is our strategy interface with one method, and sorting is the context that delegates to it:

public class DescendingComparer : IComparer<int>
{
    public int Compare(int x, int y) => y.CompareTo(x);
}

var numbers = new List<int> { 3, 1, 4, 1, 5, 9 };
numbers.Sort(new DescendingComparer());
// Result: 9, 5, 4, 3, 1, 1

We hand Sort our comparison algorithm, and it runs what we gave it without knowing what we compared. We swap the comparer, we get a new ordering, and the sorting code stays untouched.

In application code we usually meet Strategy through dependency injection, where the container is the caller that picks for us:

// Registration - we choose the strategy once, at startup.
services.AddScoped<IPaymentStrategy, CreditCardPayment>();

Our processor still receives an IPaymentStrategy and still cannot name the concrete type. Logging providers behind ILogger reach us the same way.

Common Mistakes

Reaching for the pattern when nothing varies. Two branches that will stay two branches do not repay an interface and two files. We answer structural pressure with a pattern, not every conditional we meet.

Letting our context learn the concrete type. The moment we write if (strategy is CreditCardPayment), our abstraction is gone and the conditional is back. This one arrives during a hurried bug fix, never at design time.

Feeding the strategy through a property after construction. If we set fields on our strategy afterwards, we have coupled the two again. We pass what the algorithm needs into its constructor, as we did with the account number.

Splitting one algorithm across two strategies. Each class should be a complete answer. If our caller has to run two of them in the right order, we needed a different pattern.

If the boundary between Strategy and a plain conditional feels blurry at first, that is normal. It tends to click the first time you add a third branch to somebody else’s switch.

Trade-offs

What we gain: new behavior arrives as a new class, the context stops changing, each algorithm is testable alone, and the choice can be made at startup or per request.

What it costs us: more classes than the conditional version, and one level of indirection between the call and the work. A reader following a call now has to look up which strategy was injected, and that answer lives somewhere else in the codebase.

Strategy vs Similar Patterns

Before you read each answer, decide which pattern you would name.

Strategy vs State. Both hold an object that decides behavior. With Strategy we pick the algorithm from outside and our context never changes it. With State the object swaps its own behavior as its internal condition changes, so the transitions are the point.

Strategy vs Template Method. We compose with Strategy: we inject a whole algorithm through an interface. Template Method inherits instead, fixing the step order in a base class and letting subclasses fill in single steps. Strategy replaces everything, Template Method replaces parts.

Strategy vs Command. Our strategy encapsulates an algorithm we delegate to, chosen once and run many times. Command encapsulates a request as an object, carrying its arguments and often an undo. When you see a history list, you are looking at Command.

Potential Interview Questions

What design problem does Strategy solve?
It removes our conditional for selecting an algorithm, and it moves the choice out to the caller.

How does Strategy support the Open/Closed Principle?
New behavior arrives as a new class implementing our interface. We edit nothing that already works, so we cannot break it.

When would you use a delegate instead of the full pattern?
When our algorithm is one stateless function. Once it holds data or needs a second method, the class earns its place.

How does Strategy differ from a Factory?
Strategy encapsulates interchangeable behavior, a factory encapsulates object creation. They pair well, because a factory can pick which strategy we get.

How does Strategy differ from State?
With Strategy we select and inject the behavior. With State the object transitions between behaviors on its own, driven by what happens to it.

Summary

You can now put each algorithm in its own class behind a shared interface, hand one to a context, and change the choice without reopening the context. We wrote one interface and four classes, and our processor never runs a single if.

New terms we defined here: conditional chain, context, concrete strategy, the Dependency Inversion Principle, and the Open/Closed Principle as Strategy satisfies it.

To practise spotting this pattern in code you have not seen, work through the recognition exercises in Let’s Practice!. The pattern readers confuse it with most is State, which also swaps an object behind an interface but changes that object itself as conditions change. That lesson is the useful one to read next.

0% Complete