Pattern: Strategy Type: Behavioral Importance: ⭐⭐⭐⭐⭐ Usage Frequency: Very High
The Problem: Behavior Keeps Growing
Consider a simple e-commerce system that processes payments. Initially, the system supports only credit card payments. A straightforward implementation might look like this:
public class PaymentProcessor
{
public void ProcessPayment(string paymentType)
{
if (paymentType == "CreditCard")
{
Console.WriteLine("Processing credit card...");
}
}
}
Later, new requirements introduce additional payment methods such as PayPal and Crypto. The class is extended:
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
// Handle PayPal webhook callback
// Verify PayPal transaction ID
}
else if (paymentType == "Crypto")
{
Console.WriteLine("Processing crypto...");
// Generate wallet address
// Monitor blockchain for confirmation
// Handle exchange rate fluctuation
}
else if (paymentType == "BankTransfer")
{
// ...
}
else if (paymentType == "ApplePay")
{
// ...
}
}
}
Each payment method has its own validation, error handling, and external service integration. The method grows longer with every new payment type. The problems are structural:
- Each new payment method requires modifying the same class. The method grows longer over time.
- The class becomes responsible for multiple unrelated payment behaviors.
- Testing individual payment implementations is difficult because they are tightly coupled to the processor.
- Two developers adding different payment methods will create merge conflicts in the same file.
The underlying issue is that the algorithm for processing payments varies depending on the selected method. When behavior is selected conditionally and expected to grow, the design often accumulates branching logic. This is the type of scenario the Strategy pattern addresses.
What the Strategy Pattern Is
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Instead of selecting behavior through conditional statements, each behavior is placed in a separate class that implements a common interface. The class that uses the behavior depends only on that interface. Strategy is a behavioral pattern. The essential idea: The class using the behavior should not need to know how the behavior is implemented. This allows behavior to vary independently of the context that uses it.
Structure Overview
The pattern includes:
- Strategy interface — Defines the behavior contract.
- Concrete Strategies — Implement different versions of the behavior.
- Context — Uses a Strategy through the interface. It delegates work to the strategy without knowing the implementation.
The Context depends on the abstraction, not on concrete implementations. This aligns with the Dependency Inversion Principle. The diagram looks like this: 
Refactoring the Example
Let’s put this into action and see what our concrete diagram might look like:
Let’s start by defining the Strategy interface:
public interface IPaymentStrategy
{
void ProcessPayment();
}
This interface defines a contract for processing a payment, without specifying how it is done. Next, we implement our concrete strategies:
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 encapsulates a single payment algorithm. Now we need to implement the Context:
public class PaymentProcessor
{
private readonly IPaymentStrategy _paymentStrategy;
public PaymentProcessor(IPaymentStrategy paymentStrategy)
{
_paymentStrategy = paymentStrategy;
}
public void Process()
{
_paymentStrategy.ProcessPayment();
}
}
The PaymentProcessor no longer selects the payment type using conditional logic. Instead, it receives an implementation of IPaymentStrategy and delegates the behavior.
Usage
var paypalProcessor = new PaymentProcessor(new PayPalPayment()); paypalProcessor.Process(); var creditCardProcessor = new PaymentProcessor(new CreditCardPayment()); creditCardProcessor.Process(); var cryptoProcessor = new PaymentProcessor(new CryptoPayment()); cryptoProcessor.Process();
Output:
Processing PayPal... Processing credit card... Processing crypto...
No modifications to PaymentProcessor are required to add new payment methods. A new payment type means a new class implementing IPaymentStrategy — existing code does not change.
What Changed in the Design
Original implementation:
- The processor controlled all variations through conditional logic.
- Adding a new payment method required modifying existing code.
- The class gradually accumulated responsibilities.
Refactored implementation:
- Each algorithm is isolated in its own class.
- New behaviors are added by introducing new strategy implementations.
- The context remains stable.
- The system is open for extension but closed for modification.
The central improvement is that behavior is now injected rather than selected internally.
Delegates as Lightweight Strategies
In C#, not every strategy needs a full class. When the behavior is simple — a single method with no state — a delegate or lambda can serve the same purpose:
public class PaymentProcessor
{
private readonly Action _processPayment;
public PaymentProcessor(Action processPayment)
{
_processPayment = processPayment;
}
public void Process() => _processPayment();
}
// Usage
var processor = new PaymentProcessor(() => Console.WriteLine("Processing PayPal..."));
processor.Process();
This is functionally equivalent to Strategy but without a separate interface and class hierarchy. Use delegates when the strategy is a single function with no state. Use the full pattern when strategies have state, multiple methods, or benefit from being independently testable classes. .NET itself uses this approach extensively — Func<T> and Action parameters throughout the framework are lightweight strategies. For example, List<T>.Sort(Comparison<T>) accepts a delegate that defines the comparison strategy.
When to Use Strategy
Strategy is appropriate when:
- A class contains conditional logic that selects between multiple behaviors.
- Different algorithms achieve the same goal but vary in implementation.
- Behavior should be replaceable at runtime.
- You want to follow the Open/Closed Principle.
- Dependency Injection is used to supply behavior.
Avoid Strategy when:
- Variation is minimal or unlikely to grow — a simple
if/elseis clearer. - The behavior is a single stateless function — a delegate or lambda suffices.
- The context needs to know which concrete strategy it is using — the abstraction breaks down.
Strategy in the .NET Ecosystem
Strategy is one of the most widely used patterns in .NET. IComparer<T> is a classic example — it represents a comparison strategy:
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
Different comparer implementations define different sorting behaviors. The Sort method is the context — it does not know or care how the comparison works. Other examples include logging providers behind ILogger, authentication handlers in ASP.NET Core, and custom middleware components. In practice, Strategy in .NET is most commonly used through Dependency Injection. The DI container selects the concrete strategy at application startup:
// Registration — choose the strategy once services.AddScoped<IPaymentStrategy, CreditCardPayment>(); // The PaymentProcessor receives the strategy via constructor injection // and never knows the concrete type.
This is Strategy at its most practical — the application’s composition root decides which implementation to use, and the rest of the code works through the interface.
Common Mistakes
One common mistake is introducing Strategy when variation is minimal or unlikely to grow. If the behavior has only two variants and no expectation of more, the additional abstraction may not be justified. Another mistake is replacing every conditional statement with a Strategy. Patterns should address structural design pressure, not isolated logic blocks. A single if/else that routes to two code paths does not need a pattern. A more subtle error occurs when the context becomes aware of concrete strategy types. If the context checks for specific implementations using type checks like if (strategy is CreditCardPayment), the abstraction is undermined. Strategy works best when the context treats all implementations uniformly.
Trade-offs
Let’s see what the advantages and disadvantages of this pattern are: Advantages
- Open for extension New behavior can be introduced by creating a new strategy class. Existing code in the context does not need to change.
- Supports the Open/Closed Principle The context is closed for modification but open for extension through new implementations.
- Improved testability Each strategy can be tested independently without involving the context.
- Clear separation of concerns Each algorithm is isolated in its own class, which keeps responsibilities focused and easier to reason about.
- Works naturally with Dependency Injection Strategies can be injected, configured, or swapped at runtime without altering the context.
Disadvantages
- Increased number of classes Each new variation introduces a new class, which can increase the size of the codebase.
- Additional indirection The behavior is accessed through an interface, which can make control flow slightly less direct.
- Risk of overengineering If variation is limited or unlikely to grow, introducing Strategy may add unnecessary abstraction.
Strategy vs Similar Patterns
Strategy vs State: In Strategy, the client selects the behavior and provides it to the context. In State, the object changes its own behavior when its internal state changes. Strategy vs Template Method: Strategy uses composition — behavior is injected via an interface. Template Method uses inheritance — the algorithm structure is defined in a base class with overridable steps. Strategy generally provides greater flexibility because behavior can change dynamically. Strategy vs Command: Strategy encapsulates an interchangeable algorithm that the context delegates to. Command encapsulates a request as an object, often with undo support. In the Command lesson, we saw behavior encapsulated as objects too — but Command encapsulates a request with state and undo; Strategy encapsulates an algorithm the context delegates to.
Potential Interview Questions
What design problem does Strategy solve? It removes conditional logic used to select between multiple algorithms and decouples behavior from the context. How does Strategy support the Open/Closed Principle? New behavior is added by introducing new strategy implementations without modifying the context. When would you use delegates instead of the full Strategy pattern? If the behavior is simple, stateless, and can be expressed as a single function, a delegate or lambda is sufficient. The full pattern is justified when strategies have state, multiple methods, or benefit from independent testability. How does Strategy differ from Factory patterns? Strategy encapsulates interchangeable behavior. Factory patterns encapsulate object creation. How does Strategy differ from State? In Strategy, the client selects and injects the behavior. In State, the object transitions between behaviors based on its internal state.
Summary
- Strategy encapsulates interchangeable algorithms behind a common interface.
- It eliminates conditional branching for behavior selection.
- It promotes composition over inheritance.
- In C#, delegates and lambdas serve as lightweight strategies for simple cases.
- In .NET, Strategy is most commonly applied through Dependency Injection.
- When behavior varies and is expected to evolve, Strategy provides a structured and maintainable approach.