Design Patterns & Practices  

Understanding the Strategy Pattern in C# with a Simple Example

Introduction

As applications scale, the logic behind common tasks often branches into a maze of if-else blocks and switch statements. While these conditional patterns are easy to write initially, they frequently evolve into "God objects" that are fragile and difficult to test.

The Strategy Pattern offers a more sophisticated alternative by decoupling an algorithm from its implementation. In this article, we’ll dive into:

  • The Core Concept: What defines the Strategy Pattern.

  • Identification: Recognizing the specific scenarios where this pattern shines.

  • C# Implementation: A practical, step-by-step coding example.

  • The Payoff: How this shift improves maintainability and adheres to SOLID principles.

What Is the Strategy Pattern?

The Strategy Pattern is a way to make your code "plug-and-play."

Instead of writing one massive function with a long list of if-else or switch statements, you give each specific task its own small class. Since all these classes follow the same rules, you can swap them in and out while the app is running without breaking anything.

In short:

  • The Problem: Hardcoding every choice in one giant block of code.

  • The Solution: Putting each choice into its own "module" and picking the one you need when you need it.

This helps you in many ways:

  • Cleaner Code: Your main logic doesn't get cluttered with "how-to" details.

  • Easy to Grow: To add a new feature, you just create a new class instead of editing (and potentially breaking) your existing code.

  • Better Testing: You can test each small behavior on its own.

The Problem Without Strategy Pattern

Imagine you are building a checkout system. A user needs to choose a payment method, such as a Credit Card, UPI, or PayPal.

Most developers start with a simple service like this:

public class PaymentService
{
    public void ProcessPayment(string paymentType, decimal amount)
    {
        if (paymentType == "CreditCard")
        {
            // Logic for Credit Card
            Console.WriteLine($"Paid {amount} using Credit Card");
        }
        else if (paymentType == "UPI")
        {
            // Logic for UPI
            Console.WriteLine($"Paid {amount} using UPI");
        }
        else if (paymentType == "PayPal")
        {
            // Logic for PayPal
            Console.WriteLine($"Paid {amount} using PayPal");
        }
    }
}

While this works for a small project, it quickly becomes a nightmare as you grow:

  • The "Giant Function" Problem: As you add Apple Pay, Google Pay, or Crypto, this method becomes a massive, unreadable wall of if-else statements.

  • Risky Updates: Every time you add a new payment type, you have to edit this specific file. One typo could break every other payment method.

  • Violates SOLID Principles: Specifically the Open/Closed Principle—your code should be open for new features but closed to changes in existing logic.

  • Hard to Test: You can't easily test "UPI logic" without involving the entire PaymentService class.

Applying the Strategy Pattern

We can fix the "if-else" mess by breaking the logic into three distinct parts.

Step 1: The Blueprint (Interface)

First, we create a common contract. This ensures every payment method has a Pay method, regardless of how it works inside.

public interface IPaymentStrategy
{
    void Pay(decimal amount);
}

Step 2: The Action Classes (Concrete Strategies)

Now, we give each payment method its own home. If you need to change how PayPal works, you only touch the PayPal class.

  • Credit Card: public class CreditCardPayment : IPaymentStrategy { ... }

  • UPI: public class UPIPayment : IPaymentStrategy { ... }

  • PayPal: public class PayPalPayment : IPaymentStrategy { ... }

Step 3: The Manager (Context Class)

The PaymentContext is the "boss" that holds the strategy. It doesn't care which method is being used; it just tells the strategy to "Go!"

public class PaymentContext
{
    private readonly IPaymentStrategy _strategy;

    // We "inject" the strategy here
    public PaymentContext(IPaymentStrategy strategy) => _strategy = strategy;

    public void ExecutePayment(decimal amount) => _strategy.Pay(amount);
}

Step 4: Putting it to Work

Now, you can swap behaviors like Lego bricks. Notice how we can change the logic at runtime without a single if statement:

// Pay with Credit Card
var context = new PaymentContext(new CreditCardPayment());
context.ExecutePayment(1000);

// Switch to UPI on the fly
context = new PaymentContext(new UPIPayment());
context.ExecutePayment(500);

Why this is a "Win" for your code:

  • Scalability: Want to add "Crypto"? Just create one new class. You don't have to touch your existing code.

  • Readable: No more scrolling through a 100-line switch block.

  • Clean: The PaymentContext stays small and simple.

Output

Paid 1000 using Credit Card
Paid 500 using UPI

What Did We Achieve?

Refactoring to the Strategy Pattern enhances system flexibility by removing complex if-else blocks, resulting in interchangeable payment methods and improved maintainability. This approach adheres to the Open/Closed Principle, allowing new payment methods like NetBanking to be added via new classes without modifying existing code.

When Should You Use the Strategy Pattern?

The Strategy Pattern is a powerful tool, but it is best reserved for specific scenarios. You should consider using it when:

  • You have multiple ways to perform the same task: Use it if your application needs to handle various versions of an algorithm, such as different sorting methods, data exporters (PDF vs. Excel), or payment providers.

  • You need to switch behaviors at runtime: It is the perfect choice when the specific logic needs to be decided dynamically based on user input or system state while the app is running.

  • You want to eliminate "Conditional Bloat": If you find yourself staring at massive if-else or switch blocks that handle different business rules, it’s time to refactor to a strategy.

  • You want to follow the Open/Closed Principle: Use it to ensure your system is open for extension (adding new classes) but closed for modification (leaving existing, tested code untouched).

Real-World Usage in .NET

You will encounter the Strategy Pattern throughout the .NET ecosystem and modern enterprise applications. Some of the most common use cases include: 

  • Payment Processing: Dynamically switching between providers like Stripe, PayPal, or Adyen based on a user's preference.

  • Discount & Pricing Engines: Applying different calculation rules (e.g., "Seasonal Sale," "Member Discount," or "Buy One Get One") without cluttering the checkout logic.

  • Data Export & Compression: Choosing whether to save a report as a PDF, CSV, or Excel file at the click of a button.

  • Validation Frameworks: Swapping out validation rules depending on the type of data or the region (e.g., different postal code formats for different countries).

  • Dependency Injection (DI): Modern .NET apps use DI to "inject" specific implementations of an interface into a constructor, which is essentially a built-in way to manage strategies

Conclusion

In this article, we have explored how the Strategy Pattern helps you build flexible and extensible applications by separating complex behaviors into independent classes. We’ve seen how moving away from condition-heavy logic promotes a cleaner design, making your codebase much easier to scale as your project grows