Thursday, July 30, 2026

Visitor Design Pattern

Mastering Design Patterns in C# and ASP.NET Core

Part 4.11 – Visitor Design Pattern

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


Table of Contents

  1. Introduction

  2. What is the Visitor Design Pattern?

  3. Why Do We Need the Visitor Pattern?

  4. The Problem the Visitor Pattern Solves

  5. Real-World Analogy

  6. Visitor Pattern Structure

  7. UML Class Diagram

  8. Components of the Visitor Pattern

  9. The IVisitor Interface

  10. The Accept() Method

  11. Understanding Double Dispatch

  12. Complete C# Console Application

  13. Document Processing Example

  14. Shopping Cart Example

  15. Tax Calculation Example

  16. ASP.NET Core Implementation

  17. Visitor with Dependency Injection

  18. Real-World Enterprise Examples

  19. Visitor vs Strategy

  20. Visitor vs Composite

  21. Visitor vs Interpreter

  22. Visitor vs Decorator

  23. Advantages

  24. Disadvantages

  25. Best Practices

  26. Common Mistakes

  27. Unit Testing

  28. Interview Questions

  29. When Should You Use Visitor?

  30. When Should You Avoid Visitor?

  31. Key Takeaways

  32. Conclusion

  33. Coming Up Next


1. Introduction

Imagine an application that contains many different types of objects:

Customer
Order
Invoice
Product
Employee
Department

Over time, we may need to perform many different operations on these objects:

Calculate Tax
Generate Report
Export to XML
Export to JSON
Validate
Audit
Calculate Discount
Generate Statistics

One approach is to keep adding methods to every domain class.

For example:

public class Product
{
    public void CalculateTax()
    {
    }

    public void GenerateReport()
    {
    }

    public void ExportToXml()
    {
    }

    public void ExportToJson()
    {
    }
}

This becomes difficult to maintain.

The classes gradually become responsible for too many unrelated operations.

The Visitor Design Pattern provides another approach.

Instead of putting every operation inside the domain classes, we can move operations into separate Visitor classes.


2. What is the Visitor Design Pattern?

The Visitor Design Pattern allows us to define new operations on a group of related object types without modifying the classes of those objects.

The basic idea is:

Object Structure
      |
      ↓
Accept Visitor
      |
      ↓
Visitor performs operation

For example:

              Document
                 |
       +---------+---------+
       |         |         |
       ↓         ↓         ↓
     PDF       Word       Excel
       |         |         |
       +---------+---------+
                 |
                 ↓
              Visitor
                 |
       +---------+---------+
       |         |         |
       ↓         ↓         ↓
    Export     Validate    Audit

The object structure remains stable while new operations can be added through new visitors.


3. Why Do We Need the Visitor Pattern?

Consider a document system.

We have:

PdfDocument
WordDocument
ExcelDocument

Initially we need:

Export

Later the requirements change.

We also need:

Validate
Calculate Size
Generate Report
Audit
Compress

Without Visitor, we might repeatedly modify every document class.

For example:

public class PdfDocument
{
    public void Export()
    {
    }

    public void Validate()
    {
    }

    public void CalculateSize()
    {
    }

    public void Audit()
    {
    }
}

Then we do the same for:

WordDocument
ExcelDocument
PowerPointDocument

This creates a maintenance problem.

The Visitor Pattern moves these operations into separate classes.


4. The Problem the Visitor Pattern Solves

Suppose we have:

Element A
Element B
Element C

and need several operations:

Operation 1
Operation 2
Operation 3

Without Visitor:

Element A → Operation 1
Element A → Operation 2
Element A → Operation 3

Element B → Operation 1
Element B → Operation 2
Element B → Operation 3

Element C → Operation 1
Element C → Operation 2
Element C → Operation 3

The classes become large.

With Visitor:

Element A
Element B
Element C
      |
      ↓
Accept(visitor)
      |
      ↓
Visitor
      |
      +── Operation 1
      +── Operation 2
      +── Operation 3

Operations become separate objects.


5. Real-World Analogy

Imagine a hospital with different types of patients:

Child
Adult
Senior

Different specialists visit these patients:

Doctor
Insurance Agent
Nutritionist
Auditor

The patient types remain the same.

But different visitors perform different operations.

For example:

Doctor → Examine Patient
Insurance Agent → Calculate Insurance
Nutritionist → Create Diet Plan
Auditor → Review Records

The patient doesn't need to contain every possible operation.

The visitor performs the operation.

This is the basic idea behind the Visitor Pattern.


6. Visitor Pattern Structure

The pattern typically contains:

Visitor
Concrete Visitor
Element
Concrete Element
Object Structure

The relationship looks like:

             +--------------------+
             |      Visitor       |
             +--------------------+
             | Visit(ElementA)    |
             | Visit(ElementB)    |
             +---------+----------+
                       |
             +---------+----------+
             |                    |
             ↓                    ↓
      ConcreteVisitorA     ConcreteVisitorB


             +--------------------+
             |      Element       |
             +--------------------+
             | Accept(visitor)    |
             +---------+----------+
                       |
             +---------+----------+
             |                    |
             ↓                    ↓
        ElementA             ElementB

7. UML Class Diagram

                 +-------------------------+
                 |       IVisitor          |
                 +-------------------------+
                 | + Visit(Product)        |
                 | + Visit(Service)        |
                 | + Visit(Order)          |
                 +------------+------------+
                              |
                 +------------+------------+
                 |                         |
                 ↓                         ↓
       +------------------+       +------------------+
       | TaxVisitor       |       | ReportVisitor    |
       +------------------+       +------------------+
       | Visit(Product)   |       | Visit(Product)   |
       | Visit(Service)   |       | Visit(Service)   |
       | Visit(Order)     |       | Visit(Order)     |
       +------------------+       +------------------+


                 +-------------------------+
                 |       IElement          |
                 +-------------------------+
                 | + Accept(IVisitor)      |
                 +------------+------------+
                              |
                 +------------+------------+
                 |                         |
                 ↓                         ↓
          +-------------+           +-------------+
          | Product     |           | Service     |
          +-------------+           +-------------+
          | Accept()    |           | Accept()    |
          +-------------+           +-------------+

8. Components of the Visitor Pattern

8.1 Visitor

Defines operations for each element type.

public interface IVisitor
{
    void Visit(Product product);

    void Visit(Service service);
}

8.2 Concrete Visitor

Implements a specific operation.

For example:

TaxVisitor
ReportVisitor
AuditVisitor
ExportVisitor

8.3 Element

Defines:

void Accept(IVisitor visitor);

8.4 Concrete Element

Examples:

Product
Service
Order
Invoice

Each element implements Accept().


9. The IVisitor Interface

Let's create a simple example.

public interface IVisitor
{
    void Visit(Product product);

    void Visit(Service service);
}

This means the visitor knows how to process:

Product
Service

10. The IElement Interface

public interface IElement
{
    void Accept(IVisitor visitor);
}

Each element accepts a visitor.


11. Product

public class Product : IElement
{
    public string Name { get; set; }

    public decimal Price { get; set; }

    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }
}

Notice:

visitor.Visit(this);

The current object is passed to the visitor.


12. Service

public class Service : IElement
{
    public string Name { get; set; }

    public decimal Price { get; set; }

    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }
}

Again:

visitor.Visit(this);

The visitor gets the concrete type.


13. Understanding Double Dispatch

This is one of the most important concepts in the Visitor Pattern.

Suppose we have:

element.Accept(visitor);

At first glance, it looks like one method call.

But two types determine the final behavior:

Element Type
+
Visitor Type

For example:

product.Accept(taxVisitor);

The Product class executes:

visitor.Visit(this);

Because this is a Product, the compiler selects:

Visit(Product product)

So the operation depends on both:

Product
+
TaxVisitor

This technique is known as Double Dispatch.


14. Complete C# Console Application

Let's create a complete example.

Step 1 – Visitor Interface

public interface IVisitor
{
    void Visit(Product product);

    void Visit(Service service);
}

Step 2 – Element Interface

public interface IElement
{
    void Accept(IVisitor visitor);
}

Step 3 – Product

public class Product : IElement
{
    public string Name { get; }

    public decimal Price { get; }

    public Product(
        string name,
        decimal price)
    {
        Name = name;
        Price = price;
    }

    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }
}

Step 4 – Service

public class Service : IElement
{
    public string Name { get; }

    public decimal Price { get; }

    public Service(
        string name,
        decimal price)
    {
        Name = name;
        Price = price;
    }

    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }
}

15. Tax Visitor

public class TaxVisitor : IVisitor
{
    public void Visit(Product product)
    {
        var tax = product.Price * 0.10m;

        Console.WriteLine(
            $"Product: {product.Name}");

        Console.WriteLine(
            $"Price: {product.Price:C}");

        Console.WriteLine(
            $"Tax: {tax:C}");
    }

    public void Visit(Service service)
    {
        var tax = service.Price * 0.15m;

        Console.WriteLine(
            $"Service: {service.Name}");

        Console.WriteLine(
            $"Price: {service.Price:C}");

        Console.WriteLine(
            $"Tax: {tax:C}");
    }
}

Here, the tax calculation is outside the domain classes.


16. Report Visitor

We can add another operation without modifying Product or Service.

public class ReportVisitor : IVisitor
{
    public void Visit(Product product)
    {
        Console.WriteLine(
            $"Product Report: {product.Name} - {product.Price:C}");
    }

    public void Visit(Service service)
    {
        Console.WriteLine(
            $"Service Report: {service.Name} - {service.Price:C}");
    }
}

This demonstrates one of the primary benefits of Visitor:

New operations can be introduced without modifying the element classes.


17. Program.cs

var elements = new List<IElement>
{
    new Product("Laptop", 1200m),
    new Product("Monitor", 400m),
    new Service("Consulting", 800m)
};

IVisitor taxVisitor = new TaxVisitor();

foreach (var element in elements)
{
    element.Accept(taxVisitor);
}

Console.WriteLine();

IVisitor reportVisitor =
    new ReportVisitor();

foreach (var element in elements)
{
    element.Accept(reportVisitor);
}

The same objects can be processed by different visitors.


18. Execution Flow

When this code executes:

element.Accept(taxVisitor);

the flow is:

Product
   |
   ↓
Accept(taxVisitor)
   |
   ↓
taxVisitor.Visit(this)
   |
   ↓
Visit(Product)
   |
   ↓
Calculate Product Tax

For a service:

Service
   |
   ↓
Accept(taxVisitor)
   |
   ↓
taxVisitor.Visit(this)
   |
   ↓
Visit(Service)
   |
   ↓
Calculate Service Tax

This is the essence of double dispatch.


19. Object Structure

A visitor is often used with a collection or object structure.

For example:

public class ShoppingCart
{
    private readonly List<IElement> _items =
        new();

    public void Add(IElement item)
    {
        _items.Add(item);
    }

    public void Accept(IVisitor visitor)
    {
        foreach (var item in _items)
        {
            item.Accept(visitor);
        }
    }
}

Usage:

var cart = new ShoppingCart();

cart.Add(
    new Product("Laptop", 1200m));

cart.Add(
    new Service("Consulting", 800m));

cart.Accept(
    new TaxVisitor());

Now the object structure controls traversal.


20. Shopping Cart Example

Imagine an e-commerce cart containing:

Physical Product
Digital Product
Subscription
Service

We may need operations such as:

Calculate Tax
Calculate Discount
Generate Invoice
Calculate Shipping
Generate Analytics

Instead of adding all these methods to every item, we can create:

TaxVisitor
DiscountVisitor
InvoiceVisitor
ShippingVisitor
AnalyticsVisitor

The architecture becomes:

                Shopping Cart
                     |
       +-------------+-------------+
       |             |             |
       ↓             ↓             ↓
    Product       Service      Subscription
       |             |             |
       +-------------+-------------+
                     |
                  Accept()
                     |
                     ↓
                  Visitor

21. Tax Calculation Example

A tax visitor could implement:

public class TaxCalculationVisitor : IVisitor
{
    public void Visit(Product product)
    {
        Console.WriteLine(
            $"Calculating product tax for {product.Name}");
    }

    public void Visit(Service service)
    {
        Console.WriteLine(
            $"Calculating service tax for {service.Name}");
    }
}

The important design decision is that tax calculation does not belong directly to the product or service entity.

This keeps the domain objects focused on their own responsibilities.


22. Document Processing Example

Consider a document processing system:

PdfDocument
WordDocument
ExcelDocument

Operations could include:

Export
Validate
Compress
Audit
Generate Preview

We could create:

ExportVisitor
ValidationVisitor
CompressionVisitor
AuditVisitor
PreviewVisitor

The structure becomes:

Document
   |
   +-- PDF
   +-- Word
   +-- Excel
        |
        ↓
      Accept()
        |
        ↓
      Visitor

23. ASP.NET Core Implementation

Let's create an ASP.NET Core example.

Suppose an e-commerce API has:

Product
Service
Subscription

and we want to calculate different business operations.


24. Element Interface

public interface ICartItem
{
    void Accept(ICartVisitor visitor);
}

25. Visitor Interface

public interface ICartVisitor
{
    void Visit(Product product);

    void Visit(Service service);

    void Visit(Subscription subscription);
}

26. Product

public class Product : ICartItem
{
    public string Name { get; }

    public decimal Price { get; }

    public Product(
        string name,
        decimal price)
    {
        Name = name;
        Price = price;
    }

    public void Accept(ICartVisitor visitor)
    {
        visitor.Visit(this);
    }
}

27. Service

public class Service : ICartItem
{
    public string Name { get; }

    public decimal Price { get; }

    public Service(
        string name,
        decimal price)
    {
        Name = name;
        Price = price;
    }

    public void Accept(ICartVisitor visitor)
    {
        visitor.Visit(this);
    }
}

28. Subscription

public class Subscription : ICartItem
{
    public string Name { get; }

    public decimal MonthlyPrice { get; }

    public Subscription(
        string name,
        decimal monthlyPrice)
    {
        Name = name;
        MonthlyPrice = monthlyPrice;
    }

    public void Accept(ICartVisitor visitor)
    {
        visitor.Visit(this);
    }
}

29. Pricing Visitor

public class PricingVisitor : ICartVisitor
{
    public decimal Total { get; private set; }

    public void Visit(Product product)
    {
        Total += product.Price;
    }

    public void Visit(Service service)
    {
        Total += service.Price;
    }

    public void Visit(Subscription subscription)
    {
        Total += subscription.MonthlyPrice;
    }
}

30. Registering with Dependency Injection

In ASP.NET Core:

builder.Services.AddScoped<PricingVisitor>();

We can inject the visitor into an application service.

public class CartService
{
    private readonly PricingVisitor _pricingVisitor;

    public CartService(
        PricingVisitor pricingVisitor)
    {
        _pricingVisitor = pricingVisitor;
    }

    public decimal CalculateTotal(
        IEnumerable<ICartItem> items)
    {
        foreach (var item in items)
        {
            item.Accept(_pricingVisitor);
        }

        return _pricingVisitor.Total;
    }
}

In a larger application, it is often preferable for visitors to be stateless or to return results instead of holding mutable request state.


31. Controller Example

[ApiController]
[Route("api/cart")]
public class CartController : ControllerBase
{
    private readonly CartService _cartService;

    public CartController(
        CartService cartService)
    {
        _cartService = cartService;
    }

    [HttpPost("calculate")]
    public IActionResult Calculate()
    {
        var items = new List<ICartItem>
        {
            new Product("Laptop", 1200m),
            new Service("Installation", 100m),
            new Subscription("Cloud Storage", 25m)
        };

        var total =
            _cartService.CalculateTotal(items);

        return Ok(new
        {
            Total = total
        });
    }
}

32. Visitor with Dependency Injection

Visitor Pattern and Dependency Injection work well together.

For example, a visitor may depend on:

ILogger
Repository
Tax Service
Configuration
External API Client
Audit Service

Example:

public class AuditVisitor : ICartVisitor
{
    private readonly ILogger<AuditVisitor> _logger;

    public AuditVisitor(
        ILogger<AuditVisitor> logger)
    {
        _logger = logger;
    }

    public void Visit(Product product)
    {
        _logger.LogInformation(
            "Product visited: {Name}",
            product.Name);
    }

    public void Visit(Service service)
    {
        _logger.LogInformation(
            "Service visited: {Name}",
            service.Name);
    }

    public void Visit(Subscription subscription)
    {
        _logger.LogInformation(
            "Subscription visited: {Name}",
            subscription.Name);
    }
}

This makes the Visitor suitable for enterprise scenarios where operations need external services.


33. Real-World Enterprise Example – Tax Calculation

Tax calculation can become complicated when different elements have different rules.

For example:

Product
Service
Subscription
Digital Product
Physical Product

A visitor can encapsulate the tax rules:

TaxVisitor
     |
     +-- Product Tax
     +-- Service Tax
     +-- Subscription Tax
     +-- Digital Product Tax

This avoids spreading tax calculation logic throughout the domain model.


34. Real-World Enterprise Example – Reporting

Suppose an application contains:

Customer
Order
Invoice
Payment

We need:

Financial Report
Audit Report
Management Report
Export Report

Visitors can represent these operations:

FinancialReportVisitor
AuditVisitor
ManagementReportVisitor
ExportVisitor

The domain model stays relatively stable.


35. Real-World Enterprise Example – AST / Compiler

One of the classic uses of Visitor is processing an Abstract Syntax Tree (AST).

For example:

Expression
   |
   +-- Addition
   +-- Subtraction
   +-- Multiplication
   +-- Number

Different visitors can perform:

Evaluate
Generate Code
Validate
Pretty Print
Optimize

For example:

Expression Tree
      |
      ↓
EvaluationVisitor
      |
      ↓
Calculate Result

Another visitor can process the same tree:

Expression Tree
      |
      ↓
CodeGenerationVisitor
      |
      ↓
Generate Code

This is one of the strongest examples of where Visitor can be useful.


36. Visitor vs Strategy

Both are behavioral patterns, but their goals differ.

Strategy

Strategy encapsulates an algorithm.

PaymentService
      |
      ↓
PaymentStrategy
      |
 +----+----+
 |    |    |
Card PayPal Bank

You choose which algorithm to execute.


Visitor

Visitor separates operations from a stable object structure.

Object Structure
      |
      ↓
Accept Visitor
      |
      ↓
Operation

Simple rule:

Strategy changes how something is done. Visitor adds operations to an object structure.


37. Visitor vs Composite

Composite represents a tree-like object structure:

Folder
 |
 +-- File
 +-- Folder
      |
      +-- File

Visitor can then traverse that structure and perform operations:

FileSizeVisitor
SearchVisitor
SecurityAuditVisitor

In fact, Visitor and Composite are often used together.


38. Visitor vs Interpreter

Interpreter is designed around interpreting a language or grammar.

For example:

Expression
   |
   +-- Number
   +-- Addition
   +-- Subtraction

Visitor can be used to perform operations on such expression trees.

A common relationship is:

Interpreter
    +
Visitor

where Visitor processes the resulting expression tree.


39. Visitor vs Decorator

Decorator adds behavior around an object:

Object
  ↓
Decorator
  ↓
Decorator
  ↓
Concrete Object

Visitor does not wrap the object.

Instead:

Object
  ↓
Accept(visitor)
  ↓
Visitor operation

Simple distinction

Decorator
→ Add behavior to an object

Visitor
→ Add operations to an object structure

40. Advantages

1. Adds New Operations Easily

A new visitor can be introduced without changing existing element classes.


2. Separates Operations from Data

Domain objects don't need to contain every possible operation.


3. Supports Single Responsibility

Business operations can be separated into dedicated visitor classes.


4. Useful for Stable Object Structures

If element types don't change frequently but operations change often, Visitor can be very effective.


5. Works Well with Composite

Visitor is particularly useful when processing hierarchical structures.


6. Centralizes Related Operations

For example:

TaxVisitor

can contain all tax-related logic.


41. Disadvantages

1. Adding New Element Types Is Difficult

Suppose we add:

Subscription

Every visitor interface may need:

void Visit(Subscription subscription);

and every concrete visitor must implement it.

This is the biggest trade-off.


2. Can Increase Complexity

The pattern requires:

Visitor
Concrete Visitor
Element
Concrete Element
Accept()

For simple applications, this may be unnecessary.


3. Tight Coupling Between Visitor and Elements

The visitor interface must know the concrete element types.


4. Double Dispatch Can Be Difficult for Beginners

The Accept()Visit() flow takes time to understand.


5. Not Ideal for Frequently Changing Object Structures

If new element types are added regularly, maintaining all visitors can become expensive.


42. Best Practices

1. Use Visitor When the Object Structure Is Stable

This is the most important guideline.


2. Use Visitor When Operations Change Frequently

For example:

Stable:
Product
Service
Subscription

Changing:
Tax
Audit
Reporting
Export

Visitor is a strong candidate.


3. Keep Visitors Focused

Prefer:

TaxVisitor
AuditVisitor
ExportVisitor

rather than:

EverythingVisitor

4. Avoid Stateful Visitors When Possible

Prefer visitors that return results or accumulate results in a controlled way.


5. Use Interfaces

For example:

IVisitor
IElement

This improves testability and extensibility.


6. Don't Force Visitor into Simple Applications

If there are only two classes and one operation, a normal method may be better.


7. Combine Visitor with Composite When Appropriate

For hierarchical structures, the combination can be powerful.


43. Common Mistakes

Mistake 1 – Using Visitor When Element Types Change Frequently

If new element types are constantly added, every visitor needs modification.


Mistake 2 – Creating Huge Visitors

A visitor containing hundreds of unrelated operations becomes difficult to maintain.


Mistake 3 – Ignoring Double Dispatch

Developers sometimes misunderstand why:

visitor.Visit(this);

is necessary.

It allows the concrete element type to select the correct overloaded visitor method.


Mistake 4 – Mixing Too Many Responsibilities

Each visitor should represent a meaningful operation.


Mistake 5 – Using Visitor for Simple Business Logic

Don't introduce Visitor just because it is a design pattern.

Use it when the problem actually matches the pattern.


44. Unit Testing Visitor

Visitors are usually straightforward to unit test.

Example:

[Fact]
public void TaxVisitor_ShouldCalculate_ProductTax()
{
    var product =
        new Product("Laptop", 1000m);

    var visitor =
        new TaxVisitor();

    product.Accept(visitor);

    // Assert expected tax behavior.
}

For enterprise applications, visitors can receive mocked dependencies:

TaxVisitor
   |
   +-- TaxService Mock
   +-- Logger Mock
   +-- Repository Mock

This allows isolated testing.


45. Interview Questions

Beginner

1. What is the Visitor Design Pattern?

It allows new operations to be added to an object structure without modifying the classes of the objects being operated on.


2. Which category does Visitor belong to?

Behavioral Design Pattern.


3. What are the main components?

Typically:

Visitor
Concrete Visitor
Element
Concrete Element
Object Structure

4. What is Accept()?

It is the method through which an element accepts a visitor.

void Accept(IVisitor visitor);

5. What is Visit()?

It is the visitor operation for a particular concrete element.


Intermediate

6. What is Double Dispatch?

Double Dispatch is a technique where the final method selected depends on both the runtime element and visitor types.


7. Why is visitor.Visit(this) used?

It passes the concrete element to the visitor, allowing the appropriate overloaded Visit() method to be selected.


8. What is the biggest advantage of Visitor?

Adding new operations is relatively easy without modifying existing element classes.


9. What is the biggest disadvantage?

Adding a new element type requires changes to the visitor interface and usually all concrete visitors.


10. Visitor vs Strategy?

Strategy encapsulates interchangeable algorithms.

Visitor separates operations from a relatively stable object structure.


11. Visitor vs Decorator?

Decorator wraps objects to add behavior.

Visitor performs operations on objects without wrapping them.


12. Can Visitor work with Composite?

Yes. Visitor is frequently used with Composite to traverse and process tree structures.


Advanced

13. When should you use Visitor?

When:

Object types are relatively stable
AND
Operations change frequently

14. When should you avoid Visitor?

When new element types are added frequently or the object structure is unstable.


15. Can Visitor use Dependency Injection?

Yes. Visitors can receive repositories, services, loggers, APIs, and other dependencies through ASP.NET Core DI.


16. Why is Visitor considered an advanced pattern?

Because it involves:

Polymorphism
Overloading
Double Dispatch
Object Structures
Separation of Operations

17. Can Visitor return a value?

Yes.

Instead of:

void Visit(Product product)

you can design visitors around results, for example:

decimal Visit(Product product);

or use a result object.

The appropriate approach depends on the operation.


18. Is Visitor always better than adding methods to the classes?

No.

Visitor is useful only when its trade-offs match the application's structure.


46. When Should You Use Visitor?

Visitor is a good choice when:

  • The object structure is relatively stable.

  • Many operations need to be performed on those objects.

  • New operations are added frequently.

  • You want to separate business operations from domain objects.

  • You need to process tree structures.

  • You need different algorithms over the same object hierarchy.

Typical examples:

AST Processing
Document Processing
Tax Calculation
Reporting
Auditing
Code Generation
File System Traversal
Shopping Cart Processing
Analytics
Validation

47. When Should You Avoid Visitor?

Avoid Visitor when:

  • Element types change frequently.

  • The object model is very small.

  • Only one or two operations exist.

  • The pattern would introduce unnecessary complexity.

  • Simple polymorphism can solve the problem.

  • Strategy or another pattern is a better fit.


48. Visitor Pattern in Enterprise Architecture

A possible enterprise architecture is:

                    ASP.NET Core API
                           |
                           ↓
                    Application Layer
                           |
                           ↓
                     Object Structure
                           |
             +-------------+-------------+
             |             |             |
             ↓             ↓             ↓
          Product       Service      Subscription
             |             |             |
             +-------------+-------------+
                           |
                        Accept()
                           |
                           ↓
                       Visitor
                           |
          +----------------+----------------+
          |                |                |
          ↓                ↓                ↓
       TaxVisitor      AuditVisitor    ReportVisitor

This can help keep business operations separate from domain objects.


49. Visitor + Composite

A powerful combination is:

Composite
   +
Visitor

For example, a company hierarchy:

Company
 |
 +-- Department
 |     |
 |     +-- Employee
 |     +-- Employee
 |
 +-- Department
       |
       +-- Employee
       +-- Employee

Visitors can perform:

SalaryVisitor
AuditVisitor
ReportVisitor
PerformanceVisitor

The Composite handles the hierarchy.

The Visitor handles the operation.


50. Visitor + Dependency Injection + ASP.NET Core

A modern .NET application can combine:

ASP.NET Core
      ↓
Dependency Injection
      ↓
Application Service
      ↓
Visitor
      ↓
Domain Objects

For example:

POST /api/orders/calculate-tax
                |
                ↓
         OrderController
                |
                ↓
          OrderService
                |
                ↓
           TaxVisitor
                |
        +-------+-------+
        |       |       |
        ↓       ↓       ↓
     Product Service Subscription

This can be particularly useful when visitor operations require infrastructure services.


51. Simple Memory Trick

Remember Visitor using this phrase:

"Keep the objects stable, move the operations outside."

For example:

Stable:
Product
Service
Subscription

Operations:

Tax
Audit
Report
Export

Move operations into visitors:

TaxVisitor
AuditVisitor
ReportVisitor
ExportVisitor

52. Visitor vs Other Behavioral Patterns

PatternMain Purpose
Chain of ResponsibilityPass request through handlers
CommandEncapsulate a request as an object
InterpreterInterpret a language/grammar
IteratorTraverse a collection
MediatorCentralize communication
MementoCapture and restore state
ObserverNotify dependent objects
StateChange behavior based on state
StrategyEncapsulate interchangeable algorithms
Template MethodDefine algorithm skeleton
VisitorAdd operations to object structures

53. Complete Visitor Flow

The complete flow can be remembered as:

                    Object Structure
                           |
                           ↓
                    Concrete Element
                           |
                           ↓
                    Accept(visitor)
                           |
                           ↓
                  visitor.Visit(this)
                           |
                           ↓
                 Concrete Visitor
                           |
                           ↓
                  Perform Operation

For example:

Product
   |
   ↓
Accept(TaxVisitor)
   |
   ↓
TaxVisitor.Visit(Product)
   |
   ↓
Calculate Product Tax

54. Key Takeaways

Remember these important points:

1. Visitor is a Behavioral Design Pattern

It focuses on operations performed across object structures.

2. Visitor separates operations from objects

Instead of adding every operation to domain classes, operations can be implemented in visitors.

3. Accept() is critical

public void Accept(IVisitor visitor)
{
    visitor.Visit(this);
}

4. Double Dispatch is central to the pattern

The selected operation depends on both the visitor and concrete element type.

5. Visitor is excellent when the object structure is stable

For example:

Product
Service
Subscription

while operations frequently change:

Tax
Audit
Report
Export

6. Visitor makes adding operations easier

A new visitor can represent a new operation.

7. Adding element types is expensive

If we add:

NewElement

the visitor interface and all concrete visitors may need modification.

8. Visitor and Composite work well together

Visitor can traverse and operate on hierarchical structures.


55. Conclusion

The Visitor Design Pattern provides a powerful way to separate operations from the objects on which those operations operate.

The fundamental idea is:

Object Structure
      |
      ↓
Accept Visitor
      |
      ↓
Visitor performs operation

Instead of continuously adding business operations to domain classes:

Product
   ├── Tax
   ├── Audit
   ├── Reporting
   ├── Export
   └── Validation

we can separate them:

Product
Service
Subscription
      |
      ↓
    Accept()
      |
      +── TaxVisitor
      +── AuditVisitor
      +── ReportVisitor
      +── ExportVisitor

This can make the design cleaner when the object structure is stable but operations change frequently.

However, Visitor is not a universal solution. If your application frequently adds new element types, the pattern can become expensive because every visitor must be updated.

The key rule to remember is:

Use Visitor when you have a stable object structure and need to frequently add new operations across that structure.


🎯 Behavioral Design Patterns – Complete!

With Part 4.11, we have now covered the major Behavioral Design Patterns in this series:

Part 4.1  → Chain of Responsibility
Part 4.2  → Command
Part 4.3  → Interpreter
Part 4.4  → Iterator
Part 4.5  → Mediator
Part 4.6  → Memento
Part 4.7  → Observer
Part 4.8  → State
Part 4.9  → Strategy
Part 4.10 → Template Method
Part 4.11 → Visitor

You have now completed:

                 DESIGN PATTERNS
                       |
          +------------+------------+
          |            |            |
          ↓            ↓            ↓
     Creational   Structural   Behavioral
          |            |            |
        5 Patterns   7 Patterns  11 Patterns

That's 23 classic GoF Design Patterns covered across the series.


🚀 Coming Up Next

Part 5 – Design Patterns in Real-World .NET Architecture

The next section can bring everything together by showing how these patterns are actually used in enterprise applications.

Possible topics include:

  • Design Patterns in Clean Architecture

  • Design Patterns in Microservices

  • Design Patterns in ASP.NET Core Web APIs

  • Design Patterns with Entity Framework Core

  • Design Patterns with CQRS

  • Design Patterns with MediatR

  • Design Patterns with Azure Services

  • Design Patterns in Event-Driven Architecture

  • Design Patterns in E-Commerce Systems

  • Design Patterns in Banking Applications

  • Combining Multiple Design Patterns

  • Common Design Pattern Anti-Patterns

  • How to Choose the Right Design Pattern

  • Design Patterns Interview Guide

  • Complete Real-World .NET Architecture Case Study

The next section will focus less on learning patterns individually and more on understanding how experienced .NET developers and architects combine multiple patterns to build maintainable, scalable, and enterprise-ready applications.

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.

Don't Copy

Protected by Copyscape Online Plagiarism Checker