Thursday, July 30, 2026

Strategy Design Pattern

Mastering Design Patterns in C# and ASP.NET Core

Part 4.9 – Strategy Design Pattern

Series: Design Patterns in C# and ASP.NET Core
Pattern Category: Behavioral Design Pattern
Difficulty: ⭐⭐⭐☆☆ Intermediate
Prerequisites: C#, OOP, Interfaces, SOLID Principles, Dependency Injection, ASP.NET Core


Table of Contents

  1. Introduction

  2. What is the Strategy Design Pattern?

  3. Why Do We Need the Strategy Pattern?

  4. The Problem with Large if-else and switch Statements

  5. Real-World Analogy

  6. Strategy Pattern Terminology

  7. How the Strategy Pattern Works

  8. UML Class Diagram

  9. Components of the Strategy Pattern

  10. Complete C# Console Application

  11. Payment Processing Example

  12. Discount Calculation Example

  13. ASP.NET Core Implementation

  14. Strategy Pattern with Dependency Injection

  15. Strategy Factory

  16. Real-World Enterprise Scenarios

  17. Strategy vs State

  18. Strategy vs Command

  19. Strategy vs Template Method

  20. Strategy vs Chain of Responsibility

  21. Advantages

  22. Disadvantages

  23. Best Practices

  24. Common Mistakes

  25. Unit Testing Strategies

  26. Interview Questions

  27. When Should You Use Strategy?

  28. When Should You Avoid Strategy?

  29. Key Takeaways

  30. Conclusion

  31. Coming Up Next


1. Introduction

Modern applications frequently need to perform the same business operation in multiple ways.

For example, an e-commerce application might support:

Credit Card
PayPal
Bank Transfer
Digital Wallet

All of them perform the same high-level operation:

Make Payment

However, the implementation is different.

A naive approach might look like:

if (paymentType == "CreditCard")
{
    // Credit card logic
}
else if (paymentType == "PayPal")
{
    // PayPal logic
}
else if (paymentType == "BankTransfer")
{
    // Bank transfer logic
}

This approach works initially.

But as the application grows:

Credit Card
PayPal
Bank Transfer
Apple Pay
Google Pay
Wallet
Buy Now Pay Later
Cryptocurrency
Corporate Account

the conditional logic becomes difficult to maintain.

The Strategy Design Pattern solves this problem by encapsulating each algorithm or business rule into its own class.


2. What is the Strategy Design Pattern?

The Strategy Design Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.

In simple terms:

Strategy Pattern allows us to select an algorithm or business rule at runtime without changing the code that uses it.

For example:

PaymentService
      |
      +---- CreditCardStrategy
      |
      +---- PayPalStrategy
      |
      +---- BankTransferStrategy

The application can choose the appropriate strategy at runtime.


3. Why Do We Need the Strategy Pattern?

Imagine this service:

public class PaymentService
{
    public void Pay(
        string paymentType,
        decimal amount)
    {
        if (paymentType == "CreditCard")
        {
            // Credit Card processing
        }
        else if (paymentType == "PayPal")
        {
            // PayPal processing
        }
        else if (paymentType == "BankTransfer")
        {
            // Bank Transfer processing
        }
    }
}

This creates several problems.

Problem 1 – Large Conditional Logic

The service becomes increasingly large.

Problem 2 – Difficult Maintenance

Changing one payment algorithm requires modifying the service.

Problem 3 – Violates Open/Closed Principle

Adding a new payment method requires modifying existing code.

Problem 4 – Difficult Testing

One class contains many independent algorithms.

Problem 5 – Tight Coupling

The service knows about every payment implementation.

The Strategy Pattern addresses these problems.


4. The Problem with Large if-else and switch Statements

Consider:

switch (paymentType)
{
    case "CreditCard":
        // ...
        break;

    case "PayPal":
        // ...
        break;

    case "BankTransfer":
        // ...
        break;

    case "Wallet":
        // ...
        break;
}

Now suppose the business adds:

UPI
Apple Pay
Google Pay
Corporate Card
Installment Payment

The switch keeps growing.

A better design is:

IPaymentStrategy
      |
      +---- CreditCardPaymentStrategy
      +---- PayPalPaymentStrategy
      +---- BankTransferPaymentStrategy
      +---- WalletPaymentStrategy

Now each algorithm is isolated.


5. Real-World Analogy

Consider navigation software.

You want to travel from:

Home → Airport

The destination is the same.

But you may choose:

Car
Bus
Train
Walking

The goal is the same:

Reach Destination

The algorithm changes depending on the selected strategy.

Navigation
    |
    +---- CarRouteStrategy
    +---- BusRouteStrategy
    +---- TrainRouteStrategy
    +---- WalkingRouteStrategy

This is the essence of the Strategy Pattern.


6. Strategy Pattern Terminology

The Strategy Pattern typically contains three important components.

Strategy

An interface or abstraction defining the algorithm.

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

Concrete Strategy

A class implementing a specific algorithm.

CreditCardStrategy
PayPalStrategy
BankTransferStrategy

Context

The class that uses a strategy.

PaymentService

The structure is:

             +------------------+
             |     Context      |
             +------------------+
             | - strategy       |
             +--------+---------+
                      |
                      ↓
             +------------------+
             |    IStrategy     |
             +------------------+
             | + Execute()      |
             +--------+---------+
                      |
          +-----------+-----------+
          |           |           |
          ↓           ↓           ↓
      Strategy A  Strategy B  Strategy C

7. How the Strategy Pattern Works

Suppose we have:

PaymentService

and:

IPaymentStrategy

At runtime:

User selects PayPal
        ↓
PayPalStrategy
        ↓
PaymentService
        ↓
Execute payment

For Credit Card:

User selects Credit Card
        ↓
CreditCardStrategy
        ↓
PaymentService
        ↓
Execute payment

The Context doesn't need to know how the algorithm works.

It simply calls:

_strategy.Pay(amount);

8. UML Class Diagram

                     +----------------------+
                     |   PaymentService     |
                     +----------------------+
                     | - strategy           |
                     +----------------------+
                     | + SetStrategy()      |
                     | + ProcessPayment()    |
                     +----------+-----------+
                                |
                                ↓
                     +----------------------+
                     | <<interface>>        |
                     | IPaymentStrategy     |
                     +----------------------+
                     | + Pay(amount)        |
                     +----------+-----------+
                                |
                +---------------+---------------+
                |               |               |
                ↓               ↓               ↓
      +---------------+ +---------------+ +---------------+
      | CreditCard    | | PayPal        | | BankTransfer  |
      | Strategy      | | Strategy      | | Strategy      |
      +---------------+ +---------------+ +---------------+
      | Pay()         | | Pay()         | | Pay()         |
      +---------------+ +---------------+ +---------------+

9. Components of the Strategy Pattern

9.1 Strategy Interface

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

It defines the common contract.


9.2 Concrete Strategies

public class CreditCardStrategy
    : IPaymentStrategy
{
    public void Pay(decimal amount)
    {
        Console.WriteLine(
            $"Paid ${amount} using Credit Card.");
    }
}

Another:

public class PayPalStrategy
    : IPaymentStrategy
{
    public void Pay(decimal amount)
    {
        Console.WriteLine(
            $"Paid ${amount} using PayPal.");
    }
}

9.3 Context

public class PaymentService
{
    private IPaymentStrategy _strategy;

    public PaymentService(
        IPaymentStrategy strategy)
    {
        _strategy = strategy;
    }

    public void ProcessPayment(decimal amount)
    {
        _strategy.Pay(amount);
    }
}

The Context doesn't contain payment-specific logic.


10. Complete C# Console Application

Let's build a complete example.

Step 1 – Strategy Interface

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

Step 2 – Credit Card Strategy

public class CreditCardStrategy
    : IPaymentStrategy
{
    public void Pay(decimal amount)
    {
        Console.WriteLine(
            $"Processing ${amount} using Credit Card.");

        Console.WriteLine(
            "Credit Card payment successful.");
    }
}

Step 3 – PayPal Strategy

public class PayPalStrategy
    : IPaymentStrategy
{
    public void Pay(decimal amount)
    {
        Console.WriteLine(
            $"Processing ${amount} using PayPal.");

        Console.WriteLine(
            "PayPal payment successful.");
    }
}

Step 4 – Bank Transfer Strategy

public class BankTransferStrategy
    : IPaymentStrategy
{
    public void Pay(decimal amount)
    {
        Console.WriteLine(
            $"Processing ${amount} using Bank Transfer.");

        Console.WriteLine(
            "Bank Transfer payment initiated.");
    }
}

Step 5 – Context

public class PaymentService
{
    private readonly IPaymentStrategy _strategy;

    public PaymentService(
        IPaymentStrategy strategy)
    {
        _strategy = strategy;
    }

    public void ProcessPayment(decimal amount)
    {
        _strategy.Pay(amount);
    }
}

Step 6 – Program

var creditCardPayment =
    new PaymentService(
        new CreditCardStrategy());

creditCardPayment.ProcessPayment(250);

var paypalPayment =
    new PaymentService(
        new PayPalStrategy());

paypalPayment.ProcessPayment(500);

var bankPayment =
    new PaymentService(
        new BankTransferStrategy());

bankPayment.ProcessPayment(1000);

Output:

Processing $250 using Credit Card.
Credit Card payment successful.

Processing $500 using PayPal.
PayPal payment successful.

Processing $1000 using Bank Transfer.
Bank Transfer payment initiated.

The important point is:

PaymentService

doesn't need to know how each payment algorithm works.


11. Payment Processing Example

A real-world payment application could have:

IPaymentStrategy
        |
        +-- CreditCardStrategy
        +-- DebitCardStrategy
        +-- PayPalStrategy
        +-- BankTransferStrategy
        +-- WalletStrategy

The Context simply executes:

_strategy.Pay(amount);

This makes payment methods interchangeable.


12. Discount Calculation Example

Strategy Pattern is not limited to payments.

Suppose an e-commerce application supports:

Regular Customer
Premium Customer
VIP Customer
Employee
Festival Offer

Each customer type has a different discount calculation.

Create:

public interface IDiscountStrategy
{
    decimal CalculateDiscount(decimal amount);
}

Regular customer:

public class RegularDiscountStrategy
    : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal amount)
    {
        return amount * 0.05m;
    }
}

Premium customer:

public class PremiumDiscountStrategy
    : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal amount)
    {
        return amount * 0.10m;
    }
}

VIP:

public class VipDiscountStrategy
    : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal amount)
    {
        return amount * 0.20m;
    }
}

Context:

public class DiscountService
{
    private readonly IDiscountStrategy _strategy;

    public DiscountService(
        IDiscountStrategy strategy)
    {
        _strategy = strategy;
    }

    public decimal GetDiscount(decimal amount)
    {
        return _strategy.CalculateDiscount(amount);
    }
}

Usage:

var service =
    new DiscountService(
        new VipDiscountStrategy());

var discount =
    service.GetDiscount(1000);

Console.WriteLine(
    $"Discount: ${discount}");

This is much cleaner than:

if (customerType == "Regular")
{
}
else if (customerType == "Premium")
{
}
else if (customerType == "VIP")
{
}

13. ASP.NET Core Implementation

Now let's implement the Strategy Pattern in ASP.NET Core.

Imagine an API:

POST /api/payments

Request:

{
  "amount": 500,
  "paymentMethod": "CreditCard"
}

The application should select the appropriate strategy.


14. Payment Strategy Interface

public interface IPaymentStrategy
{
    string PaymentMethod { get; }

    Task ProcessAsync(decimal amount);
}

The PaymentMethod property allows the application to identify each strategy.


15. Credit Card Strategy

public class CreditCardPaymentStrategy
    : IPaymentStrategy
{
    public string PaymentMethod =>
        "CreditCard";

    public Task ProcessAsync(decimal amount)
    {
        Console.WriteLine(
            $"Processing ${amount} using Credit Card.");

        return Task.CompletedTask;
    }
}

16. PayPal Strategy

public class PayPalPaymentStrategy
    : IPaymentStrategy
{
    public string PaymentMethod =>
        "PayPal";

    public Task ProcessAsync(decimal amount)
    {
        Console.WriteLine(
            $"Processing ${amount} using PayPal.");

        return Task.CompletedTask;
    }
}

17. Bank Transfer Strategy

public class BankTransferPaymentStrategy
    : IPaymentStrategy
{
    public string PaymentMethod =>
        "BankTransfer";

    public Task ProcessAsync(decimal amount)
    {
        Console.WriteLine(
            $"Processing ${amount} using Bank Transfer.");

        return Task.CompletedTask;
    }
}

18. Register Strategies with Dependency Injection

In Program.cs:

builder.Services.AddScoped<
    IPaymentStrategy,
    CreditCardPaymentStrategy>();

builder.Services.AddScoped<
    IPaymentStrategy,
    PayPalPaymentStrategy>();

builder.Services.AddScoped<
    IPaymentStrategy,
    BankTransferPaymentStrategy>();

ASP.NET Core can now inject:

IEnumerable<IPaymentStrategy>

and provide all registered strategies.


19. Payment Service

Instead of putting strategy-selection logic in the controller, create an application service.

public class PaymentService
{
    private readonly IEnumerable<IPaymentStrategy>
        _strategies;

    public PaymentService(
        IEnumerable<IPaymentStrategy> strategies)
    {
        _strategies = strategies;
    }

    public async Task ProcessAsync(
        string paymentMethod,
        decimal amount)
    {
        var strategy = _strategies.FirstOrDefault(
            x => x.PaymentMethod.Equals(
                paymentMethod,
                StringComparison.OrdinalIgnoreCase));

        if (strategy == null)
        {
            throw new InvalidOperationException(
                $"Unsupported payment method: {paymentMethod}");
        }

        await strategy.ProcessAsync(amount);
    }
}

Register it:

builder.Services.AddScoped<PaymentService>();

20. Request DTO

public class PaymentRequest
{
    public decimal Amount { get; set; }

    public string PaymentMethod { get; set; } = string.Empty;
}

21. Controller

[ApiController]
[Route("api/payments")]
public class PaymentsController : ControllerBase
{
    private readonly PaymentService _paymentService;

    public PaymentsController(
        PaymentService paymentService)
    {
        _paymentService = paymentService;
    }

    [HttpPost]
    public async Task<IActionResult> Process(
        PaymentRequest request)
    {
        await _paymentService.ProcessAsync(
            request.PaymentMethod,
            request.Amount);

        return Ok(new
        {
            Message = "Payment processed successfully."
        });
    }
}

Request:

POST /api/payments
{
  "amount": 750,
  "paymentMethod": "PayPal"
}

The flow becomes:

HTTP Request
     ↓
PaymentsController
     ↓
PaymentService
     ↓
Strategy Selection
     ↓
PayPalPaymentStrategy
     ↓
Payment Processing

22. Strategy Factory

When an application has many strategies, selecting the strategy using:

.FirstOrDefault(...)

may eventually become repetitive.

We can introduce a Strategy Factory.

public interface IPaymentStrategyFactory
{
    IPaymentStrategy GetStrategy(
        string paymentMethod);
}

Implementation:

public class PaymentStrategyFactory
    : IPaymentStrategyFactory
{
    private readonly IEnumerable<IPaymentStrategy>
        _strategies;

    public PaymentStrategyFactory(
        IEnumerable<IPaymentStrategy> strategies)
    {
        _strategies = strategies;
    }

    public IPaymentStrategy GetStrategy(
        string paymentMethod)
    {
        var strategy = _strategies.FirstOrDefault(
            x => x.PaymentMethod.Equals(
                paymentMethod,
                StringComparison.OrdinalIgnoreCase));

        return strategy
            ?? throw new InvalidOperationException(
                $"Unsupported payment method: {paymentMethod}");
    }
}

Register:

builder.Services.AddScoped<
    IPaymentStrategyFactory,
    PaymentStrategyFactory>();

Now the service becomes:

public class PaymentService
{
    private readonly IPaymentStrategyFactory _factory;

    public PaymentService(
        IPaymentStrategyFactory factory)
    {
        _factory = factory;
    }

    public async Task ProcessAsync(
        string paymentMethod,
        decimal amount)
    {
        var strategy =
            _factory.GetStrategy(paymentMethod);

        await strategy.ProcessAsync(amount);
    }
}

23. Strategy Pattern with Dictionary Lookup

For larger systems, a dictionary can also be used.

public class PaymentStrategyFactory
{
    private readonly Dictionary<
        string,
        IPaymentStrategy> _strategies;

    public PaymentStrategyFactory(
        IEnumerable<IPaymentStrategy> strategies)
    {
        _strategies = strategies.ToDictionary(
            x => x.PaymentMethod,
            StringComparer.OrdinalIgnoreCase);
    }

    public IPaymentStrategy GetStrategy(
        string paymentMethod)
    {
        if (_strategies.TryGetValue(
            paymentMethod,
            out var strategy))
        {
            return strategy;
        }

        throw new InvalidOperationException(
            $"Unsupported payment method: {paymentMethod}");
    }
}

This can make strategy lookup efficient and centralize selection logic.


24. Strategy Pattern and Open/Closed Principle

One of the major benefits of Strategy is its relationship with the Open/Closed Principle.

Suppose today we support:

CreditCard
PayPal
BankTransfer

Tomorrow the business requests:

Wallet

Without Strategy:

Modify PaymentService
Add another if/else
Test existing logic again

With Strategy:

Create WalletPaymentStrategy
Register it

Existing strategies don't need to change.

This is a strong example of designing software that is:

Open for extension, but closed for modification.


25. Strategy Pattern and Dependency Inversion

The Context depends on:

IPaymentStrategy

rather than:

CreditCardPaymentStrategy

This reduces coupling.

PaymentService
      |
      ↓
IPaymentStrategy
      ↑
      |
CreditCardStrategy
PayPalStrategy
BankTransferStrategy

The Context doesn't care which concrete implementation is being used.


26. Strategy Pattern in Enterprise Applications

Strategy Pattern is particularly useful for business rules.

For example:

Shipping

IShippingStrategy
   |
   +-- StandardShipping
   +-- ExpressShipping
   +-- InternationalShipping
   +-- SameDayShipping

Tax Calculation

ITaxStrategy
   |
   +-- DomesticTax
   +-- InternationalTax
   +-- StateTax
   +-- SpecialTax

Pricing

IPricingStrategy
   |
   +-- RegularPricing
   +-- PremiumPricing
   +-- WholesalePricing
   +-- PromotionalPricing

Notification

INotificationStrategy
   |
   +-- EmailNotification
   +-- SMSNotification
   +-- PushNotification

Authentication

IAuthenticationStrategy
   |
   +-- PasswordAuthentication
   +-- OAuthAuthentication
   +-- CertificateAuthentication

27. Strategy vs State

This is one of the most important interview questions.

Both patterns use interfaces and interchangeable implementations.

But their purpose is different.

Strategy

Strategy answers:

Which algorithm should I use?

Example:

PaymentService
     |
     +-- CreditCard
     +-- PayPal
     +-- BankTransfer

The client can choose a strategy.


State

State answers:

What should this object do based on its current state?

Example:

Order
 ↓
PendingState
 ↓
ConfirmedState
 ↓
ShippedState

The object's behavior changes as its state changes.

Easy way to remember

Strategy = Choose an algorithm

State = Behavior changes with state

28. Strategy vs Command

Strategy

Encapsulates an algorithm.

CalculateDiscount
CalculateTax
ProcessPayment

Command

Encapsulates a request.

CreateOrderCommand
CancelOrderCommand
RefundOrderCommand

Simple distinction

Strategy → HOW something is done

Command → WHAT operation should be performed

29. Strategy vs Template Method

Both can encapsulate algorithms.

Strategy

Uses composition.

Context
  ↓
Strategy

Template Method

Uses inheritance.

Base Class
    ↓
Concrete Class

Strategy provides runtime interchangeability.

Template Method defines the skeleton of an algorithm and allows subclasses to customize selected steps.


30. Strategy vs Chain of Responsibility

Strategy

Usually selects one algorithm.

Request
  ↓
One selected Strategy

Chain of Responsibility

A request can move through multiple handlers.

Request
 ↓
Handler A
 ↓
Handler B
 ↓
Handler C

Strategy answers:

Which algorithm should process this?

Chain of Responsibility answers:

Which handler should handle this request?


31. Strategy vs Simple Factory

These are often confused.

Simple Factory

Responsible for:

Creating an object

Example:

PaymentFactory
    ↓
Creates PayPalStrategy

Strategy

Responsible for:

Encapsulating behavior/algorithm

They can work together:

Factory
  ↓
Creates Strategy
  ↓
Context
  ↓
Executes Strategy

32. Advantages of Strategy Pattern

1. Eliminates Large Conditional Statements

Instead of:

if
else if
else if

we use independent strategy classes.


2. Supports Open/Closed Principle

New algorithms can be added without modifying the Context.


3. Improves Testability

Each strategy can be unit tested independently.


4. Reduces Coupling

The Context depends on an abstraction.


5. Improves Maintainability

Each business rule has its own class.


6. Runtime Flexibility

Strategies can be selected dynamically.


7. Promotes Single Responsibility

Each strategy focuses on one algorithm.


33. Disadvantages

1. More Classes

A simple conditional can become multiple classes.


2. More Abstraction

Developers need to understand the Strategy interface and Context.


3. Strategy Selection Still Needs Design

Something must determine which strategy to use.

For example:

Factory
Dependency Injection
Dictionary
Configuration
Business Rules

4. Can Be Overengineering

For two trivial conditions, a Strategy Pattern might be unnecessary.


5. Client May Need to Know Strategies

If strategy selection is exposed directly to callers, the client might need knowledge of available strategies.

A Factory or resolver can reduce this coupling.


34. Best Practices

1. Keep Strategies Focused

Each strategy should represent one clear algorithm or business rule.


2. Use Interfaces

Prefer:

IPaymentStrategy

over directly coupling the Context to concrete classes.


3. Use Dependency Injection

ASP.NET Core's built-in DI container works very well with Strategy Pattern.


4. Avoid Giant Strategy Classes

If a strategy contains too many unrelated responsibilities, split it.


5. Centralize Strategy Selection

Use:

Factory
Resolver
Dictionary
Keyed DI

where appropriate.


6. Give Strategies Meaningful Names

Prefer:

CreditCardPaymentStrategy
PremiumCustomerDiscountStrategy
ExpressShippingStrategy

over:

Strategy1
Strategy2
Strategy3

7. Keep the Context Simple

The Context should delegate behavior rather than recreate the algorithms.


8. Validate Strategy Inputs

A strategy should validate inputs relevant to its algorithm.


35. Common Mistakes

Mistake 1 – Strategy Interface Too Broad

Avoid:

public interface IStrategy
{
    void Execute();
    void Save();
    void SendEmail();
    void Log();
}

Keep interfaces focused.


Mistake 2 – Context Contains the Same Conditions

If you create Strategy classes but still have:

if (paymentType == "PayPal")

inside the Context, you may not be getting the full benefit of the pattern.


Mistake 3 – Creating Strategies Manually Everywhere

Avoid:

new PayPalStrategy()

throughout the application.

Use Dependency Injection or a Factory where appropriate.


Mistake 4 – Strategy Selection Scattered Across Controllers

Keep strategy selection in an application service, resolver, or factory.


Mistake 5 – Using Strategy for Simple Logic

Not every conditional needs a design pattern.


36. Unit Testing Strategies

Each strategy can be tested independently.

For example:

[Fact]
public void VipDiscount_ShouldCalculateCorrectly()
{
    var strategy =
        new VipDiscountStrategy();

    var discount =
        strategy.CalculateDiscount(1000);

    Assert.Equal(200, discount);
}

This test focuses only on:

VIP discount calculation

The other strategies can be tested separately.


37. Testing the Context

We can also test whether the Context correctly delegates to the Strategy.

Example:

var strategy =
    new CreditCardStrategy();

var service =
    new PaymentService(strategy);

service.ProcessPayment(500);

The test verifies that:

PaymentService
      ↓
CreditCardStrategy

works correctly.


38. Strategy Pattern with Modern ASP.NET Core

Modern ASP.NET Core applications provide several ways to implement Strategy Pattern.

Common approaches include:

Dependency Injection
IEnumerable<T>
Factory
Resolver
Dictionary
Keyed Services

For example, in versions supporting keyed services:

builder.Services.AddKeyedScoped<
    IPaymentStrategy,
    CreditCardPaymentStrategy>("CreditCard");

builder.Services.AddKeyedScoped<
    IPaymentStrategy,
    PayPalPaymentStrategy>("PayPal");

Then the appropriate strategy can be resolved using the key.

This can be useful when strategy selection is naturally represented by a stable key.


39. Strategy Pattern in Microservices

Strategy Pattern can be used inside microservices to isolate business rules.

For example:

Order Service
    |
    +-- Pricing Strategy
    |
    +-- Tax Strategy
    |
    +-- Shipping Strategy

An order-processing flow might look like:

Order Request
      ↓
Order Service
      ↓
Pricing Strategy
      ↓
Tax Strategy
      ↓
Shipping Strategy
      ↓
Order Total

This keeps each calculation independently replaceable.


40. Strategy Pattern + Factory Pattern

These patterns often work together.

Suppose we have:

PaymentStrategy

and:

PaymentStrategyFactory

The Factory determines:

Which strategy?

The Strategy determines:

How should the payment be processed?

Architecture:

Controller
    ↓
PaymentService
    ↓
PaymentStrategyFactory
    ↓
IPaymentStrategy
    ↓
Concrete Strategy

This is a very common enterprise design.


41. Strategy Pattern + Dependency Injection

This combination is especially useful in ASP.NET Core.

ASP.NET Core DI Container
          |
          +-- CreditCardStrategy
          +-- PayPalStrategy
          +-- BankTransferStrategy
                    |
                    ↓
             PaymentService

Advantages include:

  • Loose coupling

  • Easy unit testing

  • Easy replacement

  • Centralized configuration

  • Cleaner application services


42. Strategy Pattern + Configuration

Sometimes the strategy can be selected through configuration.

For example:

{
  "Payment": {
    "DefaultMethod": "PayPal"
  }
}

The application can select:

PayPalPaymentStrategy

without hard-coding the default behavior into the business service.

However, configuration should not replace proper validation and business rules.


43. Real-World Example: Shipping

Suppose an e-commerce platform supports:

Standard
Express
Same Day
International

Define:

public interface IShippingStrategy
{
    decimal CalculateCost(
        decimal weight,
        string destination);
}

Implement:

StandardShippingStrategy
ExpressShippingStrategy
SameDayShippingStrategy
InternationalShippingStrategy

The order service doesn't need to know the shipping formula.

It simply calls:

var cost =
    strategy.CalculateCost(
        weight,
        destination);

44. Real-World Example: Tax Calculation

Tax calculation can differ based on:

Country
State
Customer Type
Product Type
Tax Rules

Instead of:

if (country == "...")
{
}
else if (state == "...")
{
}

we can use:

ITaxStrategy
      |
      +-- USATaxStrategy
      +-- CanadaTaxStrategy
      +-- EuropeTaxStrategy

This makes tax rules easier to isolate and test.


45. Real-World Example: File Export

An application may support:

PDF
Excel
CSV
JSON
XML

Strategy:

public interface IExportStrategy
{
    Task ExportAsync(
        IEnumerable<object> data);
}

Concrete strategies:

PdfExportStrategy
ExcelExportStrategy
CsvExportStrategy
JsonExportStrategy
XmlExportStrategy

The application can choose the appropriate exporter without modifying the main business service.


46. Real-World Example: Notification

Notification systems often support:

Email
SMS
Push Notification
Teams
Webhook

Strategy:

public interface INotificationStrategy
{
    Task SendAsync(
        string message);
}

Implementations:

EmailNotificationStrategy
SmsNotificationStrategy
PushNotificationStrategy
WebhookNotificationStrategy

The notification service can dynamically select the required implementation.


47. When Should You Use Strategy?

Strategy Pattern is appropriate when:

  • There are multiple algorithms for the same task.

  • Business rules change frequently.

  • Large if-else or switch statements are growing.

  • Different customers require different rules.

  • Different payment methods require different processing.

  • Different shipping methods require different calculations.

  • You need runtime algorithm selection.

  • Algorithms should be independently testable.

  • New algorithms are likely to be added.


48. When Should You Avoid Strategy?

Don't automatically use Strategy when:

  • There is only one algorithm.

  • There are only two trivial conditions.

  • The logic is unlikely to change.

  • Additional classes would make the code harder to understand.

  • The abstraction provides little value.

Remember:

A design pattern should solve a real design problem, not simply make the code look more sophisticated.


49. Strategy Pattern – Complete Flow

The complete enterprise flow can look like this:

                    Client
                       |
                       ↓
                 API Controller
                       |
                       ↓
                 Application Service
                       |
                       ↓
                Strategy Resolver
                       |
             +---------+---------+
             |         |         |
             ↓         ↓         ↓
         Strategy A Strategy B Strategy C
             |         |         |
             +---------+---------+
                       |
                       ↓
                 Business Result

The major benefit is that the Context doesn't need to know the internal implementation of each strategy.


50. Strategy Pattern in One Example

Let's summarize with payment processing.

Without Strategy:

public void Pay(
    string type,
    decimal amount)
{
    if (type == "CreditCard")
    {
        // Logic
    }
    else if (type == "PayPal")
    {
        // Logic
    }
    else if (type == "BankTransfer")
    {
        // Logic
    }
}

With Strategy:

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

Then:

IPaymentStrategy
      |
      +-- CreditCardPaymentStrategy
      +-- PayPalPaymentStrategy
      +-- BankTransferPaymentStrategy

Context:

public class PaymentService
{
    private readonly IPaymentStrategy _strategy;

    public PaymentService(
        IPaymentStrategy strategy)
    {
        _strategy = strategy;
    }

    public void Pay(decimal amount)
    {
        _strategy.Pay(amount);
    }
}

Now:

PaymentService
      ↓
IPaymentStrategy
      ↓
Selected Algorithm

This is the Strategy Pattern.


51. Interview Questions

Beginner Questions

1. What is the Strategy Design Pattern?

It defines a family of algorithms, encapsulates each algorithm, and makes them interchangeable.


2. What problem does Strategy solve?

It eliminates complex conditional logic and allows algorithms to vary independently from the code that uses them.


3. What are the main components?

Context
Strategy
Concrete Strategy

4. What is the Context?

The class that uses a Strategy to perform an operation.


5. What is a Concrete Strategy?

A class containing one implementation of the algorithm defined by the Strategy interface.


Intermediate Questions

6. Strategy vs State?

Strategy chooses an algorithm.

State changes behavior based on the object's current state.


7. Strategy vs Command?

Strategy encapsulates an algorithm.

Command encapsulates a request.


8. Strategy vs Template Method?

Strategy uses composition.

Template Method uses inheritance.


9. How does Strategy support SOLID?

It strongly supports:

  • Single Responsibility Principle

  • Open/Closed Principle

  • Dependency Inversion Principle


10. Can Strategy Pattern be used with Dependency Injection?

Yes. ASP.NET Core DI is particularly well suited to Strategy implementations.


Advanced Questions

11. How would you implement multiple strategies in ASP.NET Core?

Register each implementation with DI and inject:

IEnumerable<IPaymentStrategy>

Then resolve the required strategy using a factory, resolver, dictionary, or another selection mechanism.


12. How would you avoid a large switch when selecting strategies?

Use:

Factory
Resolver
Dictionary
Keyed DI

depending on the application's requirements.


13. Can Strategy Pattern be used with CQRS?

Yes.

For example, a command handler could use a Strategy to select a business algorithm.


14. Can Strategy Pattern and Factory Pattern work together?

Yes.

The Factory creates or resolves the appropriate Strategy, while the Strategy encapsulates the algorithm.


15. What are the disadvantages of Strategy?

The main disadvantages are:

  • Increased number of classes

  • Additional abstraction

  • Strategy selection complexity

  • Potential overengineering


16. When would you choose Strategy instead of a switch?

Choose Strategy when the conditional branches contain substantial, independently changing algorithms or business rules.

A small, stable switch can remain simpler.


17. Can a Strategy maintain state?

It can, but strategies are often easier to reason about when they are stateless.

If state itself determines behavior, consider whether the State Pattern is more appropriate.


18. How do you unit test Strategy Pattern?

Test each Concrete Strategy independently, then test the Context or service to verify that the correct Strategy is selected and invoked.


19. Is Strategy Pattern useful in microservices?

Yes. It is useful for isolating business rules such as pricing, taxation, shipping, routing, payment processing, and notification algorithms.


20. What is the biggest sign that Strategy Pattern is needed?

A growing class with many conditional branches implementing different algorithms is a strong signal.


52. Key Takeaways

Remember these points:

1. Strategy is a Behavioral Design Pattern

It focuses on interchangeable algorithms and business rules.

2. Strategy removes complex conditional logic

Instead of:

if/else
switch

we use:

Strategy classes

3. Each strategy represents one algorithm

CreditCardStrategy
PayPalStrategy
BankTransferStrategy

4. The Context uses the Strategy

Context
   ↓
Strategy

5. Strategy supports Open/Closed Principle

Add new strategies without modifying existing algorithms.

6. Dependency Injection works extremely well with Strategy

ASP.NET Core makes Strategy registration and resolution straightforward.

7. Strategy and State are different

Strategy → Choose algorithm

State → Behavior changes based on state

8. Factory and Strategy can work together

Factory → Select/resolve strategy

Strategy → Execute algorithm

Conclusion

The Strategy Design Pattern is one of the most practical behavioral patterns for modern C# and ASP.NET Core applications.

Whenever you encounter code like:

if (type == "A")
{
    // Algorithm A
}
else if (type == "B")
{
    // Algorithm B
}
else if (type == "C")
{
    // Algorithm C
}

ask yourself:

Are these different algorithms that should be independently maintained and tested?

If the answer is yes, the Strategy Pattern may be a good fit.

The architecture becomes:

                Context
                   |
                   ↓
             IStrategy
                   |
       +-----------+-----------+
       |           |           |
       ↓           ↓           ↓
   Strategy A  Strategy B  Strategy C

Instead of putting every algorithm into one large class, each algorithm gets its own focused implementation.

In ASP.NET Core, the combination of:

Strategy Pattern
       +
Dependency Injection
       +
Factory/Resolver

can produce a clean, flexible, and maintainable architecture for complex business rules.

The key lesson is:

Use Strategy when you have multiple interchangeable algorithms or business rules and want to select the appropriate one without tightly coupling the client to their implementations.


🚀 Coming Up Next: Part 4.10 – Template Method Design Pattern

In the next article, we'll explore the Template Method Design Pattern, including:

  • What is the Template Method Pattern?

  • Why do we need it?

  • Template Method vs Strategy

  • Algorithm skeleton concept

  • Primitive operations

  • Hook methods

  • UML Class Diagram

  • Complete C# Console Application

  • Data processing example

  • Payment processing example

  • ASP.NET Core implementation

  • Abstract classes and inheritance

  • Template Method with dependency injection

  • Real-world enterprise examples

  • Advantages and disadvantages

  • Best practices

  • Common mistakes

  • Template Method vs Strategy

  • Template Method vs Factory Method

  • Template Method vs State

  • Interview questions

The Template Method Pattern will demonstrate how to define the overall structure of an algorithm in a base class while allowing derived classes to customize specific steps.

Wednesday, July 29, 2026

State Design Pattern

Mastering Design Patterns in C# and ASP.NET Core

Part 4.8 – State Design Pattern

Series: Design Patterns in C# and ASP.NET Core
Pattern Category: Behavioral Design Pattern
Difficulty: ⭐⭐⭐☆☆ Intermediate
Prerequisites: C#, OOP, Interfaces, SOLID Principles, Dependency Injection, ASP.NET Core


Table of Contents

  1. Introduction

  2. What is the State Design Pattern?

  3. Why Do We Need the State Pattern?

  4. The Problem with Large if-else and switch Statements

  5. Real-World Analogy

  6. State Pattern Terminology

  7. How the State Pattern Works

  8. UML Class Diagram

  9. Components of the State Pattern

  10. Complete C# Console Application

  11. Order Processing Example

  12. State Transitions

  13. ASP.NET Core Implementation

  14. Banking Account Example

  15. E-Commerce Example

  16. State Pattern and State Machines

  17. State vs Strategy Pattern

  18. State vs Command Pattern

  19. State vs Chain of Responsibility

  20. Advantages

  21. Disadvantages

  22. Best Practices

  23. Common Mistakes

  24. Real-World Enterprise Scenarios

  25. Interview Questions

  26. Key Takeaways

  27. Conclusion

  28. Coming Up Next


1. Introduction

In real-world applications, an object's behavior often changes depending on its current state.

Consider an e-commerce order.

An order can move through several states:

Pending
   ↓
Confirmed
   ↓
Processing
   ↓
Shipped
   ↓
Delivered

But what happens when the order is cancelled?

Pending → Cancelled
Confirmed → Cancelled
Processing → Cancelled

And perhaps:

Delivered → Cannot Cancel

A common implementation is a large if-else or switch statement:

if (order.Status == "Pending")
{
    // ...
}
else if (order.Status == "Confirmed")
{
    // ...
}
else if (order.Status == "Shipped")
{
    // ...
}

As the number of states increases, this code becomes difficult to maintain.

The State Design Pattern provides a cleaner approach by moving state-specific behavior into separate classes.


2. What is the State Design Pattern?

The State Design Pattern allows an object to change its behavior when its internal state changes.

From the outside, it can appear as though the object itself has changed its class.

The core idea is:

Context
   |
   ↓
Current State
   |
   +---- State-specific behavior

Instead of writing:

switch (order.Status)
{
    case "Pending":
        ...
        break;

    case "Confirmed":
        ...
        break;

    case "Shipped":
        ...
        break;
}

we create separate classes:

PendingState
ConfirmedState
ProcessingState
ShippedState
DeliveredState
CancelledState

Each state knows what behavior is valid for that state.


3. Why Do We Need the State Pattern?

Suppose we have:

public class Order
{
    public string Status { get; set; }

    public void Cancel()
    {
        if (Status == "Pending")
        {
            // Cancel
        }
        else if (Status == "Confirmed")
        {
            // Cancel
        }
        else if (Status == "Processing")
        {
            // Cancel
        }
        else if (Status == "Shipped")
        {
            // Cannot cancel
        }
        else if (Status == "Delivered")
        {
            // Cannot cancel
        }
    }
}

This is manageable with a few states.

But enterprise applications can have:

10+ states
20+ operations
Multiple business rules
Different permissions
Different transitions

The number of conditions can grow rapidly.

The State Pattern separates these behaviors.


4. The Problem with Large if-else and switch Statements

Imagine an order has:

Pending
Confirmed
Paid
Processing
Packed
Shipped
OutForDelivery
Delivered
Cancelled
Returned
Refunded

Now imagine operations such as:

Confirm()
Pay()
Cancel()
Ship()
Deliver()
Return()
Refund()

The code can become:

Order
 |
 +-- switch(Status)
      |
      +-- Confirm
      +-- Pay
      +-- Cancel
      +-- Ship
      +-- Deliver
      +-- Return
      +-- Refund

This leads to:

  • Large classes

  • Difficult testing

  • Repeated conditions

  • Difficult maintenance

  • High risk when adding states

  • Violations of the Open/Closed Principle

The State Pattern replaces this with:

Order
 |
 +-- PendingState
 +-- ConfirmedState
 +-- PaidState
 +-- ProcessingState
 +-- ShippedState
 +-- DeliveredState
 +-- CancelledState

5. Real-World Analogy

Consider a traffic signal.

The traffic signal has states:

RED
YELLOW
GREEN

Its behavior depends on its current state.

Red

Stop

Green

Go

Yellow

Prepare to stop

We can model this as:

TrafficLight
     |
     +---- RedState
     |
     +---- YellowState
     |
     +---- GreenState

Each state controls the behavior associated with that state.


6. State Pattern Terminology

The pattern contains three primary concepts.

Context

The object whose behavior changes.

Example:

Order

State

An interface or abstraction defining state-specific behavior.

public interface IOrderState
{
    void Handle(Order order);
}

Concrete State

Specific implementations of the State interface.

PendingState
ConfirmedState
ShippedState
DeliveredState

The structure is:

             +----------------+
             |    Context     |
             +----------------+
             | currentState   |
             +-------+--------+
                     |
                     ↓
             +---------------+
             |   IState      |
             +---------------+
             | Handle()      |
             +-------+-------+
                     |
          +----------+----------+
          |          |          |
          ↓          ↓          ↓
       State A    State B    State C

7. How the State Pattern Works

Consider an order.

Initially:

Order
 ↓
PendingState

When confirmed:

Order
 ↓
ConfirmedState

When shipped:

Order
 ↓
ShippedState

When delivered:

Order
 ↓
DeliveredState

The object's behavior changes because the current State object changes.

+----------------+
| Order Context  |
+----------------+
       |
       ↓
PendingState
       |
       ↓
ConfirmedState
       |
       ↓
ShippedState
       |
       ↓
DeliveredState

8. UML Class Diagram

A typical State Pattern UML diagram:

                 +---------------------+
                 |       Context       |
                 +---------------------+
                 | - state: IState     |
                 +---------------------+
                 | + SetState()        |
                 | + Request()         |
                 +----------+----------+
                            |
                            ↓
                 +---------------------+
                 |       IState        |
                 +---------------------+
                 | + Handle()          |
                 +----------+----------+
                            |
              +-------------+-------------+
              |             |             |
              ↓             ↓             ↓
       +-------------+ +-------------+ +-------------+
       | State A     | | State B     | | State C     |
       +-------------+ +-------------+ +-------------+
       | Handle()    | | Handle()    | | Handle()    |
       +-------------+ +-------------+ +-------------+

9. Components of the State Pattern

9.1 Context

The Context maintains the current state.

public class Order
{
    private IOrderState _state;

    public Order(IOrderState state)
    {
        _state = state;
    }

    public void SetState(IOrderState state)
    {
        _state = state;
    }

    public void Process()
    {
        _state.Handle(this);
    }
}

9.2 State Interface

public interface IOrderState
{
    void Handle(Order order);
}

9.3 Concrete States

public class PendingState : IOrderState
{
    public void Handle(Order order)
    {
        Console.WriteLine(
            "Order is pending.");
    }
}

Another:

public class ShippedState : IOrderState
{
    public void Handle(Order order)
    {
        Console.WriteLine(
            "Order has been shipped.");
    }
}

10. Complete C# Console Application

Let's build a complete order-processing example.

Step 1 – State Interface

public interface IOrderState
{
    void Confirm(Order order);

    void Ship(Order order);

    void Deliver(Order order);

    void Cancel(Order order);
}

The state interface defines operations that can behave differently based on the current state.


11. Context – Order

public class Order
{
    private IOrderState _state;

    public Order()
    {
        _state = new PendingState();
    }

    public void SetState(IOrderState state)
    {
        _state = state;
    }

    public void Confirm()
    {
        _state.Confirm(this);
    }

    public void Ship()
    {
        _state.Ship(this);
    }

    public void Deliver()
    {
        _state.Deliver(this);
    }

    public void Cancel()
    {
        _state.Cancel(this);
    }
}

Notice that Order does not contain:

if status == Pending
if status == Confirmed
if status == Shipped

The behavior is delegated to the current state.


12. Pending State

public class PendingState : IOrderState
{
    public void Confirm(Order order)
    {
        Console.WriteLine(
            "Order confirmed.");

        order.SetState(
            new ConfirmedState());
    }

    public void Ship(Order order)
    {
        Console.WriteLine(
            "Cannot ship a pending order.");
    }

    public void Deliver(Order order)
    {
        Console.WriteLine(
            "Cannot deliver a pending order.");
    }

    public void Cancel(Order order)
    {
        Console.WriteLine(
            "Order cancelled.");

        order.SetState(
            new CancelledState());
    }
}

13. Confirmed State

public class ConfirmedState : IOrderState
{
    public void Confirm(Order order)
    {
        Console.WriteLine(
            "Order is already confirmed.");
    }

    public void Ship(Order order)
    {
        Console.WriteLine(
            "Order shipped.");

        order.SetState(
            new ShippedState());
    }

    public void Deliver(Order order)
    {
        Console.WriteLine(
            "Cannot deliver before shipping.");
    }

    public void Cancel(Order order)
    {
        Console.WriteLine(
            "Order cancelled.");

        order.SetState(
            new CancelledState());
    }
}

14. Shipped State

public class ShippedState : IOrderState
{
    public void Confirm(Order order)
    {
        Console.WriteLine(
            "Order is already shipped.");
    }

    public void Ship(Order order)
    {
        Console.WriteLine(
            "Order is already shipped.");
    }

    public void Deliver(Order order)
    {
        Console.WriteLine(
            "Order delivered.");

        order.SetState(
            new DeliveredState());
    }

    public void Cancel(Order order)
    {
        Console.WriteLine(
            "Cannot cancel a shipped order.");
    }
}

15. Delivered State

public class DeliveredState : IOrderState
{
    public void Confirm(Order order)
    {
        Console.WriteLine(
            "Order is already delivered.");
    }

    public void Ship(Order order)
    {
        Console.WriteLine(
            "Order is already delivered.");
    }

    public void Deliver(Order order)
    {
        Console.WriteLine(
            "Order is already delivered.");
    }

    public void Cancel(Order order)
    {
        Console.WriteLine(
            "Cannot cancel a delivered order.");
    }
}

16. Cancelled State

public class CancelledState : IOrderState
{
    public void Confirm(Order order)
    {
        Console.WriteLine(
            "Cannot confirm a cancelled order.");
    }

    public void Ship(Order order)
    {
        Console.WriteLine(
            "Cannot ship a cancelled order.");
    }

    public void Deliver(Order order)
    {
        Console.WriteLine(
            "Cannot deliver a cancelled order.");
    }

    public void Cancel(Order order)
    {
        Console.WriteLine(
            "Order is already cancelled.");
    }
}

17. Program

var order = new Order();

order.Confirm();

order.Ship();

order.Deliver();

order.Cancel();

Output:

Order confirmed.
Order shipped.
Order delivered.
Cannot cancel a delivered order.

The important point is that the same:

order.Cancel();

method behaves differently depending on the current state.


18. Understanding the State Transition

The order starts here:

PendingState

After:

order.Confirm();

it becomes:

ConfirmedState

Then:

order.Ship();

changes it to:

ShippedState

Then:

order.Deliver();

changes it to:

DeliveredState

Therefore:

Pending
   |
   | Confirm()
   ↓
Confirmed
   |
   | Ship()
   ↓
Shipped
   |
   | Deliver()
   ↓
Delivered

19. Order Cancellation Flow

Another possible transition:

Pending
   |
   | Cancel()
   ↓
Cancelled

Or:

Confirmed
   |
   | Cancel()
   ↓
Cancelled

But:

Shipped
   |
   | Cancel()
   ↓
Not Allowed

This is where the State Pattern becomes very useful.


20. State Transition Diagram

For a larger order workflow:

                         +-------------+
                         |   Pending   |
                         +------+------+
                                |
                             Confirm
                                |
                                ↓
                         +-------------+
                         |  Confirmed  |
                         +------+------+
                                |
                              Ship
                                |
                                ↓
                         +-------------+
                         |   Shipped   |
                         +------+------+
                                |
                             Deliver
                                |
                                ↓
                         +-------------+
                         |  Delivered  |
                         +-------------+

Pending ----------------------> Cancelled
Confirmed --------------------> Cancelled

This is effectively a simple state machine.


21. ASP.NET Core Implementation

Let's build a more realistic ASP.NET Core example.

Suppose we have:

Order API

and the order has states:

Pending
Confirmed
Shipped
Delivered
Cancelled

We can use dependency injection to manage the states.


22. State Interface

public interface IOrderState
{
    string Name { get; }

    Task ConfirmAsync(OrderContext context);

    Task ShipAsync(OrderContext context);

    Task DeliverAsync(OrderContext context);

    Task CancelAsync(OrderContext context);
}

23. Order Context

public class OrderContext
{
    private IOrderState _state;

    public int OrderId { get; }

    public OrderContext(
        int orderId,
        IOrderState initialState)
    {
        OrderId = orderId;
        _state = initialState;
    }

    public string StateName =>
        _state.Name;

    public void SetState(
        IOrderState state)
    {
        _state = state;
    }

    public Task ConfirmAsync()
    {
        return _state.ConfirmAsync(this);
    }

    public Task ShipAsync()
    {
        return _state.ShipAsync(this);
    }

    public Task DeliverAsync()
    {
        return _state.DeliverAsync(this);
    }

    public Task CancelAsync()
    {
        return _state.CancelAsync(this);
    }
}

24. Pending State

public class PendingOrderState
    : IOrderState
{
    public string Name => "Pending";

    public Task ConfirmAsync(
        OrderContext context)
    {
        Console.WriteLine(
            $"Order {context.OrderId} confirmed.");

        return Task.CompletedTask;
    }

    public Task ShipAsync(
        OrderContext context)
    {
        throw new InvalidOperationException(
            "Pending order cannot be shipped.");
    }

    public Task DeliverAsync(
        OrderContext context)
    {
        throw new InvalidOperationException(
            "Pending order cannot be delivered.");
    }

    public Task CancelAsync(
        OrderContext context)
    {
        Console.WriteLine(
            $"Order {context.OrderId} cancelled.");

        return Task.CompletedTask;
    }
}

For a production application, the state transition would generally also update persistent order state in a database.


25. Dependency Injection

Register the states:

builder.Services.AddTransient<
    PendingOrderState>();

builder.Services.AddTransient<
    ConfirmedOrderState>();

builder.Services.AddTransient<
    ShippedOrderState>();

builder.Services.AddTransient<
    DeliveredOrderState>();

builder.Services.AddTransient<
    CancelledOrderState>();

You can also register them against a common abstraction:

builder.Services.AddTransient<
    IOrderState,
    PendingOrderState>();

builder.Services.AddTransient<
    IOrderState,
    ConfirmedOrderState>();

builder.Services.AddTransient<
    IOrderState,
    ShippedOrderState>();

builder.Services.AddTransient<
    IOrderState,
    DeliveredOrderState>();

builder.Services.AddTransient<
    IOrderState,
    CancelledOrderState>();

Then ASP.NET Core can resolve all implementations through:

IEnumerable<IOrderState>

26. Controller Example

[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
    private readonly IEnumerable<IOrderState>
        _states;

    public OrdersController(
        IEnumerable<IOrderState> states)
    {
        _states = states;
    }

    [HttpPost("{id}/confirm")]
    public IActionResult Confirm(int id)
    {
        var state = _states
            .First(x => x.Name == "Pending");

        var order = new OrderContext(
            id,
            state);

        order.ConfirmAsync();

        return Ok(new
        {
            OrderId = id,
            State = "Confirmed"
        });
    }
}

In a real application, you would normally load the current state from the database rather than constructing it directly inside the controller.


27. Better Enterprise Architecture

For an enterprise application, avoid putting state-management logic directly inside the controller.

A better structure is:

Controller
    ↓
Application Service
    ↓
Order State Manager
    ↓
State Object
    ↓
Repository
    ↓
Database

For example:

POST /api/orders/100/ship
            |
            ↓
      OrdersController
            |
            ↓
      OrderService
            |
            ↓
     Current State
            |
            ↓
    ShippedState
            |
            ↓
    Update Database

This keeps responsibilities separated.


28. Database State vs State Pattern

This is an important enterprise consideration.

The database might contain:

OrderId = 1001
Status = "Shipped"

The State Pattern then maps the persisted status to behavior:

"Pending"   → PendingState
"Confirmed" → ConfirmedState
"Shipped"   → ShippedState
"Delivered" → DeliveredState

So:

Database
   ↓
Current Status
   ↓
State Object
   ↓
Behavior

This approach is useful when state-specific business behavior is complex.


29. Banking Account Example

A bank account can also have states.

For example:

Active
Suspended
Blocked
Closed

Operations:

Deposit
Withdraw
Transfer
Close

Behavior can depend on the state.

Active

Deposit → Allowed
Withdraw → Allowed
Transfer → Allowed

Suspended

Deposit → Allowed
Withdraw → Restricted
Transfer → Restricted

Closed

Deposit → Not Allowed
Withdraw → Not Allowed
Transfer → Not Allowed

Instead of:

if (status == "Active")
{
    ...
}
else if (status == "Suspended")
{
    ...
}
else if (status == "Closed")
{
    ...
}

each state can define the appropriate behavior.


30. E-Commerce Example

An order workflow might look like:

Pending
   ↓
PaymentProcessing
   ↓
Paid
   ↓
Packing
   ↓
Shipped
   ↓
Delivered

Additional states:

Cancelled
PaymentFailed
Returned
Refunded

State-specific operations can include:

Cancel
Ship
Return
Refund
RetryPayment

This can quickly become complicated using only conditionals.

The State Pattern allows the behavior to be distributed across state classes.


31. State Pattern and State Machines

The State Pattern is closely related to Finite State Machines (FSMs).

An FSM consists of:

States
Events
Transitions
Actions

For example:

                Confirm
Pending ------------------> Confirmed
   |                           |
   | Cancel                    | Ship
   ↓                           ↓
Cancelled                    Shipped
                                |
                                | Deliver
                                ↓
                            Delivered

This can be represented as:

Current State
      +
Event
      ↓
Transition
      ↓
New State

32. State Pattern vs State Machine

They are related but not identical.

State Pattern

Primarily focuses on:

Object behavior based on current state

State Machine

Primarily focuses on:

Valid states
Events
Transitions
Transition rules

A complex enterprise workflow may use a state-machine library or a dedicated workflow engine instead of implementing every transition manually.


33. State vs Strategy Pattern

This is one of the most common interview questions.

They look similar because both use composition and interfaces.

Strategy

The client chooses an algorithm.

OrderService
     |
     +---- CreditCardStrategy
     +---- PayPalStrategy
     +---- BankTransferStrategy

Example:

Choose payment algorithm

State

The object's behavior changes because its state changes.

Order
 ↓
PendingState
 ↓
ConfirmedState
 ↓
ShippedState

Key Difference

Strategy:

Which algorithm should I use?

State:

What behavior is appropriate for my current state?


34. State vs Command

Command

Encapsulates a request or operation.

ShipOrderCommand
CancelOrderCommand
RefundOrderCommand

State

Controls behavior based on the object's current state.

PendingState
ShippedState
DeliveredState

For example:

Command = CancelOrder
State = Shipped

The Shipped State can determine:

Cancel → Not Allowed

So the two patterns can work together.


35. State vs Chain of Responsibility

State

The object has one current state:

Order
 ↓
Current State

Chain of Responsibility

A request moves through a sequence of handlers:

Request
 ↓
Handler A
 ↓
Handler B
 ↓
Handler C

State determines behavior based on current condition.

Chain of Responsibility determines which handler should process a request.


36. Advantages of State Pattern

1. Eliminates Large Conditional Statements

Instead of:

if
else if
else if
else if

we use:

State Classes

2. Single Responsibility

Each state handles its own behavior.


3. Easier Maintenance

Changes to ShippedState don't necessarily affect PendingState.


4. Better Extensibility

Adding:

ReturnedState

can be done independently.


5. Clear State Transitions

Transitions can be expressed explicitly:

Pending → Confirmed
Confirmed → Shipped
Shipped → Delivered

6. Better Testability

Each state can be unit tested independently.


37. Disadvantages

1. More Classes

A simple if statement may become:

PendingState.cs
ConfirmedState.cs
ShippedState.cs
DeliveredState.cs
CancelledState.cs

This can be unnecessary for simple workflows.


2. State Transition Complexity

With many states, transitions themselves can become difficult to manage.


3. Increased Abstraction

Developers need to understand:

Context
State
Concrete States
Transitions

4. Persistence Complexity

If state is stored in a database, the application needs a reliable mapping between:

Database State

and:

State Object

5. Overengineering Risk

Don't use State Pattern simply because an application has two or three statuses.

Use it when state-specific behavior is sufficiently complex to justify the abstraction.


38. Best Practices

1. Use Strongly Typed State Representation

Avoid scattering strings such as:

"Pending"
"Shipped"
"Delivered"

throughout the code.

Prefer centralized state definitions.


2. Keep State Classes Focused

Each state should contain behavior relevant to that state.


3. Make Invalid Transitions Explicit

For example:

Delivered → Ship

should clearly be rejected.


4. Keep Persistence Separate

Don't make state classes responsible for every database operation.

Prefer:

State
 ↓
Application Service
 ↓
Repository

where appropriate.


5. Unit Test State Transitions

Test:

Pending → Confirmed
Confirmed → Shipped
Shipped → Delivered

and invalid transitions.


6. Document the State Diagram

For complex workflows, a state-transition diagram can be extremely valuable.


7. Consider a State Machine for Complex Workflows

If there are dozens of states and transitions, a dedicated state-machine approach may be easier to maintain than hand-written state classes.


39. Common Mistakes

Mistake 1 – Using State Pattern for Every Enum

Not every enum requires a State Pattern.


Mistake 2 – Mixing State and Persistence

Avoid making every state class directly responsible for database access.


Mistake 3 – Allowing Invalid Transitions

Make transitions explicit.


Mistake 4 – Creating Huge State Classes

If one state class becomes enormous, reconsider your responsibilities.


Mistake 5 – Circular State Dependencies

Be careful when state objects directly create each other:

PendingState
   ↓
ConfirmedState
   ↓
ShippedState

For complex applications, a state manager or factory can centralize state creation.


Mistake 6 – Ignoring Concurrency

In enterprise applications, two requests could attempt:

Ship Order
Cancel Order

at almost the same time.

State validation alone does not solve database concurrency.

Use appropriate transactional and concurrency mechanisms.


40. Real-World Enterprise Scenarios

The State Pattern can be useful in:

E-Commerce

Pending
Confirmed
Paid
Packed
Shipped
Delivered
Returned
Refunded

Banking

Active
Suspended
Blocked
Closed

Payment Processing

Created
Processing
Authorized
Captured
Failed
Refunded

Insurance Claims

Submitted
UnderReview
Approved
Rejected
Settled
Closed

Loan Processing

ApplicationSubmitted
UnderReview
Approved
Rejected
Disbursed
Closed

Ticketing Systems

Open
Assigned
InProgress
Resolved
Closed
Reopened

Document Approval

Draft
Submitted
UnderReview
Approved
Rejected
Published

41. State Pattern with SOLID Principles

The State Pattern naturally supports several SOLID principles.

Single Responsibility Principle

Each state class focuses on state-specific behavior.


Open/Closed Principle

New states can often be added without modifying every existing state.


Dependency Inversion Principle

The Context works with:

IOrderState

rather than concrete implementations.


42. State Pattern Testing

Suppose we have:

Pending
Confirmed
Shipped
Delivered

We should test valid transitions.

Test 1

Pending → Confirmed

Test 2

Confirmed → Shipped

Test 3

Shipped → Delivered

Test 4

Delivered → Cancelled

Expected:

Rejected

Test 5

Pending → Delivered

Expected:

Rejected

State-based testing becomes much easier when each state's behavior is isolated.


43. Interview Questions

Beginner

1. What is the State Design Pattern?

It allows an object to change its behavior when its internal state changes.


2. What problem does the State Pattern solve?

It helps eliminate complex conditional logic where behavior depends heavily on an object's current state.


3. What are the main components?

Context
State
Concrete State

4. What is the Context?

The object whose behavior changes depending on its current state.


5. What is a Concrete State?

A class implementing behavior for a particular state.


Intermediate

6. How does State reduce if-else statements?

Instead of:

if (status == "Pending")
{
}
else if (status == "Shipped")
{
}

we use:

PendingState
ShippedState

Each class contains the appropriate behavior.


7. State Pattern vs Strategy Pattern?

Strategy chooses an algorithm.

State changes behavior based on the object's current state.


8. State Pattern vs Command?

Command encapsulates a request.

State determines how the object behaves when that request is received.


9. State Pattern vs Chain of Responsibility?

State represents behavior based on current state.

Chain of Responsibility passes a request through handlers.


10. Can State Pattern work with Dependency Injection?

Yes.

ASP.NET Core can register state implementations and inject them through abstractions such as:

IEnumerable<IOrderState>

44. Advanced Interview Questions

11. When should you use State Pattern instead of an enum?

Use the State Pattern when states have significantly different behavior or transition rules.

If the enum is only used for display or simple comparisons, State Pattern may be unnecessary.


12. Can State Pattern be used with databases?

Yes.

The database stores the current state, and the application maps that state to the appropriate behavior object.


13. Is State Pattern suitable for microservices?

It can be used within an individual service.

For distributed workflows, however, additional tools may be appropriate:

  • State machines

  • Workflow engines

  • Durable messaging

  • Saga orchestration

  • Event-driven architecture


14. How do you prevent invalid state transitions?

Centralize transition rules and explicitly reject invalid operations.

For example:

Delivered
   |
   +-- Cancel → Not Allowed
   +-- Ship   → Not Allowed

15. How would you persist state?

A common approach is:

Order
----------------
Id
Status
CreatedDate
UpdatedDate

The Status is persisted in the database.

At runtime:

Status
 ↓
State Factory
 ↓
Concrete State

16. What is the difference between State Pattern and State Machine?

State Pattern focuses on object behavior based on state.

A state machine focuses more explicitly on states, events, transitions, and transition rules.


17. Can State Pattern improve testability?

Yes.

Each concrete state can be tested independently.


18. Is State Pattern always better than switch?

No.

For a small number of simple states, a switch can be clearer.

The State Pattern becomes valuable when state-specific behavior becomes complex and frequently changes.


19. Can State objects be stateless?

Yes.

If a State object contains no instance-specific data, it can potentially be reused depending on the application's design.


20. What is a major warning sign that State Pattern is needed?

A strong indication is a class containing many repeated conditions such as:

if (status == ...)

across many methods, where each status causes substantially different behavior.


45. Practical Architecture Example

A production e-commerce system could look like:

                    API Request
                        |
                        ↓
                OrdersController
                        |
                        ↓
                  OrderService
                        |
                        ↓
                Current Order State
                        |
         +--------------+--------------+
         |              |              |
         ↓              ↓              ↓
    PendingState   ShippedState   DeliveredState
         |              |              |
         +--------------+--------------+
                        |
                        ↓
                  Order Repository
                        |
                        ↓
                     Database

For example:

POST /api/orders/100/ship

The application:

1. Loads Order 100
2. Reads current status
3. Creates/resolves corresponding State
4. Executes Ship()
5. Validates transition
6. Changes state
7. Persists new status
8. Publishes an event if necessary

46. State Pattern + Observer Pattern

The State Pattern can also work together with the Observer Pattern.

For example:

Order
 ↓
State Changes
 ↓
ShippedState
 ↓
OrderStatusChanged Event
 ↓
Observers
 ├── Email
 ├── SMS
 ├── Audit
 └── Analytics

Here:

State Pattern handles:

What behavior is valid for the current state?

Observer Pattern handles:

Who needs to know that the state changed?

This combination is extremely useful in enterprise applications.


47. State Pattern + Command Pattern

These patterns can also complement one another.

For example:

CancelOrderCommand
        |
        ↓
      Order
        |
        ↓
   Current State
        |
        ↓
Can Cancel?

If the order is:

PendingState

then:

Cancel → Allowed

If it is:

ShippedState

then:

Cancel → Rejected

So:

Command = What operation is requested?

State = Is that operation valid, and how should it behave?

48. State Pattern + CQRS

The State Pattern can also be useful in applications implementing CQRS.

For example:

ShipOrderCommand
       |
       ↓
ShipOrderHandler
       |
       ↓
Order
       |
       ↓
Current State
       |
       ↓
ShippedState

The State Pattern handles state-specific domain behavior, while CQRS separates commands and queries.

These patterns solve different problems and can work together.


49. When Should You Use the State Pattern?

Use State Pattern when:

  • An object has many distinct states.

  • Behavior changes significantly between states.

  • State-specific rules are becoming complex.

  • Large if-else or switch statements are growing.

  • State transitions are important business rules.

  • You need to test each state independently.

  • New states are expected to be introduced over time.


50. When Should You NOT Use It?

Avoid State Pattern when:

  • There are only one or two simple states.

  • State-specific behavior is trivial.

  • A simple switch is much clearer.

  • The abstraction creates more classes than value.

  • There are no meaningful state transitions.

Remember:

Design patterns are tools, not mandatory rules.


51. Key Takeaways

The most important concepts to remember are:

1. State is a Behavioral Design Pattern

It focuses on changing behavior based on an object's state.

2. It reduces complex conditional logic

Instead of:

Large if/else

use:

State classes

3. The Context owns the current State

Context
   ↓
Current State

4. State transitions are important

Pending
   ↓
Confirmed
   ↓
Shipped
   ↓
Delivered

5. State and Strategy are different

Strategy selects an algorithm.

State represents behavior associated with the current state.

6. State and Command can work together

Command represents an operation.

State determines whether and how that operation should execute.

7. State and Observer can work together

State handles behavior.

Observer handles notification of state changes.

8. State machines are useful for complex workflows

For highly complex workflows, consider dedicated state-machine or workflow solutions.


Conclusion

The State Design Pattern is an excellent solution when an object's behavior changes significantly depending on its current state.

Instead of creating a massive class containing:

if (...)
else if (...)
else if (...)
else if (...)

we can model each state separately:

PendingState
ConfirmedState
ShippedState
DeliveredState
CancelledState

The architecture becomes:

                    Context
                       |
                       ↓
                  Current State
                       |
          +------------+------------+
          |            |            |
          ↓            ↓            ↓
       Pending      Shipped      Delivered

This approach improves:

  • Maintainability

  • Readability

  • Testability

  • Extensibility

  • Separation of responsibilities

In modern .NET applications, the State Pattern is particularly useful for:

  • Order workflows

  • Payment processing

  • Banking systems

  • Insurance claims

  • Approval workflows

  • Ticketing systems

  • Document workflows

  • Loan processing

  • Business process management

The key lesson is:

Use the State Pattern when an object's behavior changes substantially according to its current state and conditional logic is becoming difficult to maintain.


🚀 Coming Up Next: Part 4.9 – Strategy Design Pattern

In the next article, we'll explore the Strategy Design Pattern, including:

  • What is the Strategy Pattern?

  • Why do we need it?

  • Encapsulating algorithms

  • Replacing large if-else and switch statements

  • Strategy and Context concepts

  • UML Class Diagram

  • Complete C# Console Application

  • Payment processing example

  • Discount calculation example

  • ASP.NET Core implementation

  • Dependency Injection with Strategy

  • Strategy Factory

  • Strategy vs State

  • Strategy vs Command

  • Strategy vs Template Method

  • Strategy vs Chain of Responsibility

  • Real-world enterprise scenarios

  • Advantages and disadvantages

  • Best practices

  • Common mistakes

  • Interview questions

The Strategy Pattern is one of the most useful patterns for modern .NET applications because it allows algorithms and business rules to be changed independently without modifying the code that uses them.

Don't Copy

Protected by Copyscape Online Plagiarism Checker