Waqas Ahmad — Software Architect & Technical Consultant - Available USA, Europe, Global

Waqas Ahmad — Software Architect & Technical Consultant

Specializing in

Distributed Systems

.NET ArchitectureCloud-Native ArchitectureAzure Cloud EngineeringAPI ArchitectureMicroservices ArchitectureEvent-Driven ArchitectureDatabase Design & Optimization

👋 Hi, I'm Waqas — a Software Architect and Technical Consultant specializing in .NET, Azure, microservices, and API-first system design..
I help companies build reliable, maintainable, and high-performance backend platforms that scale.

Experienced across engineering ecosystems shaped by Microsoft, the Cloud Native Computing Foundation, and the Apache Software Foundation.

Available for remote consulting (USA, Europe, Global) — flexible across EST, PST, GMT & CET.

services
Article

Behavioral Design Patterns in .NET: All 11 Patterns with Full Working Code

GoF behavioral patterns in .NET: Strategy, Observer, Command, and more. With C# examples.

services
Read the article

Introduction

This guidance is relevant when the topic of this article applies to your system or design choices; it breaks down when constraints or context differ. I’ve applied it in real projects and refined the takeaways over time (as of 2026).

Behavioral design patterns govern how objects interact and share responsibility—unlike creational or structural patterns, they address communication and flow of control. This article covers all eleven GoF behavioral patterns with clear definitions, when and why to use each, class-structure diagrams, and full working C# examples you can run or adapt. Choosing the right pattern matters for architects and developers who need testable, maintainable object interactions without over-engineering.

If you are new to behavioral patterns, start with All behavioral patterns at a glance and jump to the pattern you need.

For a deeper overview of this topic, explore the full .NET Architecture guide.

What are behavioral patterns?

Behavioral patterns in the Gang of Four catalog describe recurring ways that objects collaborate and delegate work. They help you avoid tangled conditionals, tight coupling between producers and consumers, and duplicated control flow. There are eleven behavioral patterns; the table below lists all of them. This article explains and implements every one with full C# examples.

Pattern Problem it solves Typical .NET use
Chain of Responsibility Pass a request along a chain of handlers until one handles it Middleware pipelines, validation chains
Command Encapsulate a request as an object (undo, queue, log) CQRS, MediatR, handlers
Interpreter Interpret a language or expression grammar DSLs, expression trees, parsers
Iterator Access aggregate elements without exposing structure IEnumerable<T>, yield return
Mediator Centralize object interactions to reduce coupling Chat rooms, form coordinators
Memento Capture and restore an object’s state (undo/snapshot) Undo/redo, save/restore
Observer One source of events, many subscribers Events, IObservable<T>, message brokers
State Behavior depends on internal state State classes, workflows
Strategy Multiple interchangeable algorithms Interface + DI
Template Method Same algorithm skeleton, variable steps Abstract base class + overrides
Visitor Add operations to object structure without changing it Double dispatch, AST traversal

Decision Context

  • System scale: Applies to any codebase where object collaboration and control flow matter—from small apps to large; behavioral patterns (Strategy, Observer, Command, State, etc.) are used per feature or component, not “whole system.”
  • Team size: One to several teams; someone must recognise when a pattern fits (e.g. “this is a Strategy” or “we need a Mediator”) so the codebase doesn’t fill with ad hoc conditionals.
  • Time / budget pressure: Fits when you have time to introduce an interface or abstraction; overkill for one-off scripts or throwaway code.
  • Technical constraints: .NET and C#; patterns map to interfaces, delegates, events, and base classes; no special framework required beyond the language.
  • Non-goals: This article does not optimize for “use every pattern everywhere”; it optimises for choosing the right pattern when the problem (communication, state, undo, etc.) matches.

All behavioral patterns at a glance

  • Chain of Responsibility: A request is passed along a chain of handler objects. Each handler either handles the request or passes it to the next. Use for middleware, validation pipelines, or when you have multiple candidates that might handle a request.
  • Command: A request is encapsulated as an object, so you can parameterize clients, queue or log requests, and support undo/redo. Use for CQRS, job queues, audit logs, and API handlers.
  • Interpreter: Define a grammar for a (small) language and interpret sentences. Use for DSLs, expression evaluation, or parsing rules.
  • Iterator: Provide a way to access elements of an aggregate sequentially without exposing its internal representation. .NET uses IEnumerable<T> and IEnumerator<T>.
  • Mediator: Define an object that encapsulates how a set of objects interact, so they don’t reference each other directly. Use for chat rooms, wizard steps, or form validation that involves many controls.
  • Memento: Capture an object’s internal state so it can be restored later, without exposing that state. Use for undo/redo or save/restore.
  • Observer: One object (subject) notifies many dependents (observers) when its state changes. Use for event-driven UI, domain events, or one-to-many notifications.
  • State: An object’s behavior changes with its internal state. Model each state as a class; the context delegates to the current state. Use for workflows, connection state, or wizards.
  • Strategy: Define a family of algorithms, encapsulate each, and make them interchangeable. The context uses an interface so you can swap implementations at runtime. Use for pricing, validation, or serialization.
  • Template Method: Define the skeleton of an algorithm in a base class; subclasses override specific steps. Use for ETL, report generation, or fixed flows with variable steps.
  • Visitor: Add new operations to a structure of objects without changing their classes. Use for double dispatch, AST traversal, or when you have many element types and many operations.

Strategy pattern

What it is and when to use it

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The context (e.g. Salary) depends on an abstraction (IAllowanceStrategy) and delegates the variable part to it, so you can add or swap strategies without changing the context. Use it when different policies (e.g. company-specific salary allowances, pricing rules, tax calculation, validation) must be pluggable at runtime or via configuration. In .NET you typically inject the strategy via constructor (DI) or resolve it from a factory by key.

Class structure

Loading diagram…

Class structure explained: The strategy interface (IAllowanceStrategy) defines a single method: CalculateAllowance(baseAmount, context). Concrete strategies (CompanyAAllowanceStrategy, CompanyBAllowanceStrategy, CompanyCAllowanceStrategy) implement that interface with different rules (e.g. housing + transport, lump sum, performance bonus). The context (Salary) holds a reference to IAllowanceStrategy (injected via constructor), holds the base amount, and calls _allowanceStrategy.CalculateAllowance(BaseAmount, context) when it needs the total. The context never branches on company or strategy type; it only knows the interface. To add a new company or policy, add a new strategy class and wire it (e.g. via DI or a factory by company id); Salary stays unchanged.

Full working example: Salary and company-specific allowances

Scenario: You need to compute total salary (base + allowances) for employees. Different companies use different allowance rules: Company A uses housing (15% of base) + fixed transport; Company B uses a single benefits lump sum (20% of base); Company C uses a performance bonus (10% of base if rating ≥ 4). The Salary type must work for all companies without holding company-specific fields. The Strategy pattern lets each company supply its own IAllowanceStrategy implementation; Salary only delegates to the strategy.

1. Strategy interface and context data

namespace StrategyExample.Salary;

public interface IAllowanceStrategy
{
    decimal CalculateAllowance(decimal baseAmount, SalaryContext context);
}

public class SalaryContext
{
    public string CompanyId { get; set; } = "";
    public string EmployeeId { get; set; } = "";
    public int YearsOfService { get; set; }
    public decimal PerformanceRating { get; set; }
}

2. Concrete strategies (one per company policy)

namespace StrategyExample.Salary;

public class CompanyAAllowanceStrategy : IAllowanceStrategy
{
    public decimal CalculateAllowance(decimal baseAmount, SalaryContext context)
    {
        var housing = baseAmount * 0.15m;
        var transport = 500m;
        return housing + transport;
    }
}

public class CompanyBAllowanceStrategy : IAllowanceStrategy
{
    public decimal CalculateAllowance(decimal baseAmount, SalaryContext context) =>
        baseAmount * 0.20m;
}

public class CompanyCAllowanceStrategy : IAllowanceStrategy
{
    public decimal CalculateAllowance(decimal baseAmount, SalaryContext context) =>
        context.PerformanceRating >= 4.0m ? baseAmount * 0.10m : 0m;
}

3. Context: Salary uses the strategy

namespace StrategyExample.Salary;

public class Salary
{
    private readonly IAllowanceStrategy _allowanceStrategy;
    public decimal BaseAmount { get; set; }

    public Salary(IAllowanceStrategy allowanceStrategy) => _allowanceStrategy = allowanceStrategy;

    public decimal GetTotal(SalaryContext context) =>
        BaseAmount + _allowanceStrategy.CalculateAllowance(BaseAmount, context);
}

// Usage:
// var salary = new Salary(new CompanyAAllowanceStrategy()) { BaseAmount = 5000 };
// var total = salary.GetTotal(new SalaryContext { CompanyId = "A", EmployeeId = "E1" }); // 5000 + 1250 = 6250

How this code fits together: (1) The context (Salary) receives an IAllowanceStrategy in its constructor (in production, resolve by company id from a factory or DI). (2) Salary holds only BaseAmount; it has no allowance-specific properties. (3) When the caller needs the total, it calls GetTotal(context). Salary computes allowance = _allowanceStrategy.CalculateAllowance(BaseAmount, context) and returns BaseAmount + allowance. (4) Each concrete strategy implements CalculateAllowance with one company’s rules; Salary never branches on company type. (5) To support a new company, add a new class that implements IAllowanceStrategy and register it; Salary and existing strategies are unchanged.

When to use Strategy: Use whenever you have multiple interchangeable algorithms (allowance rules, pricing, tax, validation, serialization) and you want to add or change them without changing the type that uses them. In .NET, register strategies in DI and inject a single strategy or a factory that selects by key (company id, tenant, feature flag).


Observer pattern

What it is and when to use it

The Observer pattern defines a one-to-many dependency: when one object (the subject or observable) changes state, all its dependents (observers) are notified and updated. It is the backbone of event-driven and reactive designs. Use it when you have a single source of truth (e.g. order created, price changed) and multiple, unrelated consumers (UI, warehouse, analytics) that must react without the source knowing their types.

.NET supports this natively with events (event EventHandler<T>) and with IObservable<T> / IObserver<T> for a push-based, composable model. Message brokers (Azure Service Bus, RabbitMQ) extend the idea across process boundaries.

Class structure

The diagram below shows the main types and how they relate.

Loading diagram…

Class structure explained: The diagram shows the main types. The subject (here OrderNotifier or OrderService) holds a list of observers and exposes Subscribe(observer) so that any component can register; it returns an IDisposable so callers can unsubscribe. When something meaningful happens (e.g. an order is created), the subject calls NotifyOrderCreated (or equivalent), which loops over all observers and calls their notification method (OnNext or OnOrderCreated). Concrete observers (WarehouseObserver, AnalyticsObserver) implement the same interface but do different work—one reserves stock, the other records analytics. The subject never depends on concrete observer types; it only knows the interface. That keeps the subject decoupled from who reacts and lets you add or remove observers without changing the subject.

Full working example: Order created notifications

1. Event payload and observer interface

namespace ObserverExample;

public record OrderCreatedEvent(Guid OrderId, string CustomerId, decimal Total, DateTime OccurredAt);

public interface IOrderObserver
{
    void OnOrderCreated(OrderCreatedEvent evt);
}

2. Subject: order service that notifies observers

namespace ObserverExample;

public class OrderService
{
    private readonly List<IOrderObserver> _observers = new();
    private readonly IOrderRepository _repo;

    public OrderService(IOrderRepository repo) => _repo = repo;

    public IDisposable Subscribe(IOrderObserver observer)
    {
        _observers.Add(observer);
        return new Subscription(() => _observers.Remove(observer));
    }

    public async Task<Order> CreateOrderAsync(string customerId, decimal total, CancellationToken ct = default)
    {
        var order = new Order { Id = Guid.NewGuid(), CustomerId = customerId, Total = total };
        await _repo.AddAsync(order, ct);

        var evt = new OrderCreatedEvent(order.Id, order.CustomerId, order.Total, DateTime.UtcNow);
        foreach (var observer in _observers.ToList())
            observer.OnOrderCreated(evt);

        return order;
    }

    private sealed class Subscription : IDisposable
    {
        private readonly Action _unsubscribe;
        public Subscription(Action unsubscribe) => _unsubscribe = unsubscribe;
        public void Dispose() => _unsubscribe?.Invoke();
    }
}

3. Concrete observers

namespace ObserverExample;

public class WarehouseObserver : IOrderObserver
{
    private readonly IWarehouseService _warehouse;
    public WarehouseObserver(IWarehouseService warehouse) => _warehouse = warehouse;

    public void OnOrderCreated(OrderCreatedEvent evt) =>
        _warehouse.ReserveForOrderAsync(evt.OrderId, evt.Total).AsTask().GetAwaiter().GetResult();
}

public class AnalyticsObserver : IOrderObserver
{
    private readonly IAnalyticsService _analytics;
    public AnalyticsObserver(IAnalyticsService analytics) => _analytics = analytics;

    public void OnOrderCreated(OrderCreatedEvent evt) =>
        _analytics.TrackOrderCreated(evt.OrderId, evt.CustomerId, evt.Total);
}

4. Using .NET events (alternative)

public class OrderServiceWithEvents
{
    public event EventHandler<OrderCreatedEvent>? OrderCreated;

    public async Task<Order> CreateOrderAsync(string customerId, decimal total, CancellationToken ct = default)
    {
        var order = new Order { Id = Guid.NewGuid(), CustomerId = customerId, Total = total };
        await _repo.AddAsync(order, ct);
        OrderCreated?.Invoke(this, new OrderCreatedEvent(order.Id, order.CustomerId, order.Total, DateTime.UtcNow));
        return order;
    }
}

// Subscriber: orderService.OrderCreated += (_, e) => warehouse.ReserveForOrder(e.OrderId);

How this code fits together: (1) At startup or when a screen loads, one or more observers call Subscribe(observer) and receive an IDisposable. (2) When CreateOrderAsync runs, the order is saved to the repository, then an OrderCreatedEvent is built with the new order’s data. (3) The service iterates over _observers.ToList() and calls OnOrderCreated(evt) on each observer. (4) WarehouseObserver calls the warehouse service to reserve stock; AnalyticsObserver sends the event to analytics. (5) When a subscriber is done (e.g. view closed), it calls Dispose() on the subscription so it is removed from the list.

Explanation of the code: OrderCreatedEvent is a small, immutable DTO so all observers get the same data. Subscribe returns IDisposable so callers can unsubscribe and avoid leaks. Using _observers.ToList() before iterating avoids modification-during-iteration if an observer unsubscribes inside its handler. The inner Subscription class simply runs the unsubscribe action when Dispose is called. The OrderServiceWithEvents variant uses C# events instead of a list; subscribers use += and -= ; the subject invokes OrderCreated?.Invoke(this, evt).

Practical example: In an e-commerce checkout, when an order is placed you might notify: (1) Inventory to reserve items, (2) CRM to update the customer’s order history, (3) Analytics to record the sale, (4) Email service to send a confirmation. The order service stays unaware of these; it just raises one event. In a microservices setup, the same idea applies with a message broker: the order service publishes OrderCreated to a topic, and multiple subscribers (warehouse, analytics, notifications) consume it independently. At BAT we used this pattern for order-created: one event, multiple handlers (fulfillment, reporting, audit).

When to use Observer: Domain events, UI binding, audit trails, integration with external systems. Prefer IObservable<T> when you need composition, cancellation, or Rx-style operators; use C# events when the surface is small and local.


Command pattern

What it is and when to use it

The Command pattern encapsulates a request as an object, so you can parameterize clients with different requests, queue or log requests, and support undo or redo. Handlers process commands without the sender knowing the receiver. This fits CQRS (separate read/write models), job queues, audit logs, and API handlers. In .NET, MediatR popularizes command/query objects with IRequest<T> and IRequestHandler<TRequest, TResponse>.

Class structure

The diagram below shows the main types and how they relate.

Loading diagram…

Class structure explained: The diagram shows the main types. A command (CreateOrderCommand) is a small object that carries the data for one request (e.g. customer ID and items). It implements a marker or generic ICommand so the dispatcher can route it. A handler (CreateOrderHandler) implements ICommandHandler<TCommand, TResponse>: it receives the command and performs the work (e.g. create order, save to repository). The sender (controller, queue consumer) does not call the handler directly; it sends the command to a dispatcher (or MediatR), which finds the right handler and invokes Handle(cmd). The sender and handler are decoupled: the sender only knows the command; the handler only knows how to process that command. That enables queuing, logging, undo (if the command stores enough to reverse), and CQRS.

Full working example: CQRS-style commands

1. Command and handler interfaces

namespace CommandExample;

public interface ICommand { }

public interface ICommandHandler<in TCommand, TResponse> where TCommand : ICommand
{
    Task<TResponse> Handle(TCommand command, CancellationToken ct = default);
}

2. Create order command and handler

namespace CommandExample;

public record CreateOrderCommand(string CustomerId, List<LineItemDto> Items) : ICommand;

public record LineItemDto(string ProductId, int Quantity, decimal UnitPrice);

public class CreateOrderHandler : ICommandHandler<CreateOrderCommand, OrderResult>
{
    private readonly IOrderRepository _repo;
    public CreateOrderHandler(IOrderRepository repo) => _repo = repo;

    public async Task<OrderResult> Handle(CreateOrderCommand cmd, CancellationToken ct = default)
    {
        var order = new Order
        {
            Id = Guid.NewGuid(),
            CustomerId = cmd.CustomerId,
            Lines = cmd.Items.Select(i => new OrderLine(i.ProductId, i.Quantity, i.UnitPrice)).ToList(),
            Status = "Created"
        };
        await _repo.AddAsync(order, ct);
        return new OrderResult(order.Id, order.Status);
    }
}

public record OrderResult(Guid OrderId, string Status);

3. Undoable command (for undo/redo)

public interface IUndoableCommand : ICommand
{
    void Undo();
}

public class SetOrderStatusCommand : IUndoableCommand
{
    public Guid OrderId { get; init; }
    public string NewStatus { get; init; } = "";
    private string? _previousStatus;

    public void Execute(OrderRepository repo)
    {
        var order = repo.Get(OrderId);
        _previousStatus = order.Status;
        order.Status = NewStatus;
    }
    public void Undo()
    {
        var order = _repo.Get(OrderId);
        order.Status = _previousStatus!;
    }
}

4. MediatR-style registration

// builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<CreateOrderHandler>());
// Send: await _mediator.Send(new CreateOrderCommand(customerId, items));

How this code fits together: (1) The sender (e.g. API controller) creates a CreateOrderCommand(customerId, items) and sends it to a dispatcher (e.g. IMediator.Send(command)). (2) The dispatcher finds the handler registered for CreateOrderCommand (e.g. CreateOrderHandler) and resolves it from DI. (3) The handler’s Handle(cmd) runs: it builds an Order from the command, saves it via _repo.AddAsync, and returns OrderResult. (4) The sender gets back OrderResult without knowing the handler or repository. For undo, a command like SetOrderStatusCommand stores _previousStatus in Execute and restores it in Undo.

Explanation of the code: CreateOrderCommand is a record so it is immutable and easy to serialize (for queues or audit). The handler is generic: ICommandHandler<CreateOrderCommand, OrderResult> so one handler per command type. The handler receives IOrderRepository via constructor; the command itself has no behaviour—only data. IUndoableCommand adds Undo() so you can store commands in a stack and pop/undo when the user requests it.

Practical example: In an API you might have CreateOrderCommand, UpdateOrderCommand, CancelOrderCommand. Each has a handler; the controller only calls _mediator.Send(command). In a job queue, commands are serialized and enqueued; a worker deserializes and dispatches to the handler. For audit, you log every command (and optionally the result) so you can replay or debug. At BAT we used command/handler for CQRS: write operations were commands; handlers updated the domain and published events. Undo/redo in a UI is the same idea: each user action is a command; undo pops the stack and calls Undo().

When to use Command: CQRS, job queues, undo/redo, audit logs, API handlers. Keep commands small and handlers focused on one use case.


State pattern

What it is and when to use it

The State pattern lets an object change its behavior when its internal state changes. Instead of large switch or if chains, you model each state as a class and delegate behavior to the current state object. The context holds the current state and forwards requests to it; states can transition the context to another state. Use it for order lifecycles (Draft → Submitted → Shipped), connection state, UI wizards, or any finite state machine.

Class structure

Loading diagram…

Class structure explained: The diagram shows the main types. The context (Order) holds the current state (IOrderState) and exposes operations like Submit(), Ship(), Cancel(). Instead of implementing logic itself, the context delegates to the current state: State.Submit(this), State.Ship(this). Each concrete state (DraftState, SubmittedState, ShippedState) implements the same interface but with different behaviour: in DraftState, Submit transitions to SubmittedState; Ship throws. In SubmittedState, Ship transitions to ShippedState. Transitions are done by the state calling order.SetState(new SubmittedState()) so the context never branches on state type—the state objects encapsulate both behaviour and transition logic.

Full working example: Order workflow

1. State interface and context

namespace StateExample;

public interface IOrderState
{
    void Submit(Order order);
    void Ship(Order order);
    void Cancel(Order order);
    string StatusName { get; }
}

public class Order
{
    public Guid Id { get; set; }
    public string CustomerId { get; set; } = "";
    internal IOrderState State { get; private set; }

    public Order() => State = new DraftState();
    public string Status => State.StatusName;

    public void SetState(IOrderState state) => State = state;
    public void Submit() => State.Submit(this);
    public void Ship() => State.Ship(this);
    public void Cancel() => State.Cancel(this);
}

2. Concrete states

namespace StateExample;

public class DraftState : IOrderState
{
    public string StatusName => "Draft";
    public void Submit(Order order) => order.SetState(new SubmittedState());
    public void Ship(Order order) => throw new InvalidOperationException("Cannot ship a draft order.");
    public void Cancel(Order order) { /* already draft; no-op or remove */ }
}

public class SubmittedState : IOrderState
{
    public string StatusName => "Submitted";
    public void Submit(Order order) { /* no-op */ }
    public void Ship(Order order) => order.SetState(new ShippedState());
    public void Cancel(Order order) => order.SetState(new DraftState());
}

public class ShippedState : IOrderState
{
    public string StatusName => "Shipped";
    public void Submit(Order order) { }
    public void Ship(Order order) { }
    public void Cancel(Order order) => throw new InvalidOperationException("Cannot cancel a shipped order.");
}

Usage

var order = new Order { Id = Guid.NewGuid(), CustomerId = "C1" };
Console.WriteLine(order.Status); // Draft
order.Submit();
Console.WriteLine(order.Status); // Submitted
order.Ship();
Console.WriteLine(order.Status); // Shipped

How this code fits together: (1) The context (Order) starts with State = new DraftState(). (2) When the user calls order.Submit(), the context delegates to State.Submit(this). (3) DraftState.Submit(order) calls order.SetState(new SubmittedState()), so the context’s state changes. (4) Later, order.Ship() calls State.Ship(this); SubmittedState.Ship(order) transitions to ShippedState. (5) Invalid actions (e.g. Ship from DraftState) throw or no-op; each state owns its rules.

Explanation of the code: Order does not contain if (status == Draft); it only holds State and forwards calls. SetState is internal (or public) so state classes can transition the context. Each state class is small: one responsibility (e.g. “Draft behaviour”). StatusName on the interface lets the context expose the current state name without casting. New states (e.g. CancelledState) are added by creating a new class and wiring transitions; the context code does not change.

Practical example: In an order management system, an order moves Draft → Submitted → Shipped (or Draft → Cancelled). In a document workflow you might have Draft → InReview → Approved → Published. In a connection component you might have Disconnected → Connecting → Connected → Reconnecting. Each state knows what it allows and what the next state is. At BAT we used the State pattern for order lifecycle so adding a new status (e.g. “OnHold”) meant adding a new state class and updating transitions in the relevant states—no giant switch in the order entity.

When to use State: Workflows (order, document, approval), connection state, wizards. Keep transition logic inside state classes and the context thin.


Template Method pattern

What it is and when to use it

The Template Method pattern defines the skeleton of an algorithm in a base class and lets subclasses override specific steps without changing the overall structure. The base class has one template method that calls abstract or virtual steps in order; subclasses implement the abstract steps and optionally override hook methods (virtual methods with empty defaults). Use it for ETL pipelines, report generation, or any “same steps, different implementations” flow.

Class structure

The diagram below shows the main types and how they relate.

Loading diagram…

Class structure explained: The diagram shows the main types. The abstract base class (DataImporter) defines the template method (ImportAsync) that fixes the algorithm: validate → load → transform → save. It calls abstract methods (LoadAsync, SaveAsync) that subclasses must implement, and hook methods (ValidateAsync, TransformAsync) that are virtual with empty or default implementations so subclasses can optionally override them. Concrete subclasses (CsvImporter, JsonImporter) implement the abstract steps (e.g. LoadAsync reads from file, SaveAsync writes to repository) and may override hooks (e.g. CsvImporter overrides TransformAsync to parse CSV to JSON). The base class never knows the concrete type; it only calls the abstract and virtual methods. The flow is fixed; only the steps vary.

Full working example: ETL pipeline

1. Abstract base with template and hooks

namespace TemplateMethodExample;

public abstract class DataImporter
{
    public async Task ImportAsync(CancellationToken ct = default)
    {
        await ValidateAsync(ct);
        var raw = await LoadAsync(ct);
        var transformed = await TransformAsync(raw, ct);
        await SaveAsync(transformed, ct);
    }

    protected virtual Task ValidateAsync(CancellationToken ct) => Task.CompletedTask; // hook
    protected abstract Task<byte[]> LoadAsync(CancellationToken ct);
    protected virtual Task<byte[]> TransformAsync(byte[] raw, CancellationToken ct) => Task.FromResult(raw); // hook
    protected abstract Task SaveAsync(byte[] data, CancellationToken ct);
}

2. Concrete importer: CSV

namespace TemplateMethodExample;

public class CsvImporter : DataImporter
{
    private readonly string _path;
    private readonly IRepository _repo;
    public CsvImporter(string path, IRepository repo) { _path = path; _repo = repo; }

    protected override Task<byte[]> LoadAsync(CancellationToken ct) =>
        File.ReadAllBytesAsync(_path, ct);

    protected override Task<byte[]> TransformAsync(byte[] raw, CancellationToken ct)
    {
        var text = Encoding.UTF8.GetString(raw);
        var rows = text.Split('
').Select(line => line.Split(','));
        var json = JsonSerializer.SerializeToUtf8Bytes(rows);
        return Task.FromResult(json);
    }

    protected override Task SaveAsync(byte[] data, CancellationToken ct) =>
        _repo.SaveAsync("csv_import", data, ct);
}

3. Concrete importer: JSON (reuses hook)

public class JsonImporter : DataImporter
{
    private readonly string _path;
    private readonly IRepository _repo;
    public JsonImporter(string path, IRepository repo) { _path = path; _repo = repo; }

    protected override Task<byte[]> LoadAsync(CancellationToken ct) =>
        File.ReadAllBytesAsync(_path, ct);

    protected override Task SaveAsync(byte[] data, CancellationToken ct) =>
        _repo.SaveAsync("json_import", data, ct);
    // TransformAsync uses default (identity) from base
}

How this code fits together: (1) The caller invokes importer.ImportAsync() on a concrete instance (e.g. CsvImporter). (2) The template method in the base runs: first ValidateAsync (hook; default no-op), then LoadAsync (abstract; CsvImporter reads the file), then TransformAsync (hook; CsvImporter parses CSV to JSON, JsonImporter uses default identity), then SaveAsync (abstract; both write to repository). (3) Each step is polymorphic: the base calls this.LoadAsync() and the runtime invokes the overriding method. (4) New importers (e.g. ExcelImporter) add a new subclass and implement only LoadAsync and SaveAsync (and optionally TransformAsync).

Explanation of the code: ImportAsync is the template method: it defines the skeleton and calls the steps in order. Abstract methods (LoadAsync, SaveAsync) force subclasses to fill in the variable parts. Hook methods (ValidateAsync, TransformAsync) have default implementations (empty or identity) so subclasses can override only when needed—JsonImporter does not override TransformAsync. Using protected keeps the steps invisible to callers; only ImportAsync is public.

Practical example: In a data pipeline you might have CSV → DB, Excel → DB, API → DB: same flow (validate, load, transform, save), different sources and transforms. In report generation the template might be fetch data → apply filters → render → export; subclasses vary the source (SQL, API) and format (PDF, Excel). In tests you might have a FakeImporter that overrides LoadAsync and SaveAsync to use in-memory data. At BAT we used Template Method for ETL jobs so adding a new file type (e.g. Parquet) meant one new subclass; the orchestration and error handling stayed in the base.

When to use Template Method: ETL, report generation, data migration. Prefer composition (Strategy) if you need to swap entire algorithms at runtime; use Template Method when the algorithm structure is fixed and only steps vary.


Chain of Responsibility pattern

What it is and when to use it

Chain of Responsibility passes a request along a chain of handler objects. Each handler either performs its work and then passes the request to the next handler, or short-circuits (e.g. returns failure) without calling the next. The sender sends the request only to the first handler; it does not know how many handlers exist or in what order they run. Use it for middleware pipelines (e.g. ASP.NET Core middleware), validation pipelines (validate → authorize → rate-limit → process), or any flow where multiple handlers might participate and you want to add or reorder them without changing the sender.

Class structure

Loading diagram…

Class structure explained: The handler interface (IRequestHandler<T>) defines SetNext(next) (to build the chain) and HandleAsync(request) (to process or pass along). An abstract base (RequestHandlerBase<T>) holds _next and provides CallNextAsync(request) so concrete handlers can pass the request to the next link. Concrete handlers (ValidationHandler, AuthHandler, LoggingHandler) implement HandleAsync: they do their work (e.g. validate, check auth, log), then call CallNextAsync to pass the request down the chain, or return false to stop. The client builds the chain (validation.SetNext(auth).SetNext(logging)) and sends the request to the first handler only. Order of handlers is determined by how you link them, not by the sender.

Full working example: Create-order pipeline (validate → auth → log)

Scenario: A CreateOrderRequest must pass through validation (customer ID and item count valid), then auth (caller is authenticated), then logging (record the request). If validation or auth fails, the chain stops and the request is not logged or processed. Each step is a separate handler; the API only calls the first handler.

1. Handler interface and base

namespace ChainOfResponsibilityExample;

public interface IRequestHandler<T>
{
    IRequestHandler<T> SetNext(IRequestHandler<T> next);
    Task<bool> HandleAsync(T request, CancellationToken ct = default);
}

public abstract class RequestHandlerBase<T> : IRequestHandler<T>
{
    private IRequestHandler<T>? _next;
    public IRequestHandler<T> SetNext(IRequestHandler<T> next) { _next = next; return next; }
    protected async Task<bool> CallNextAsync(T request, CancellationToken ct) =>
        _next != null ? await _next.HandleAsync(request, ct) : true;
    public abstract Task<bool> HandleAsync(T request, CancellationToken ct = default);
}

public class CreateOrderRequest { public string CustomerId { get; set; } = ""; public int ItemCount { get; set; } }

2. Concrete handlers

namespace ChainOfResponsibilityExample;

public class ValidationHandler : RequestHandlerBase<CreateOrderRequest>
{
    public override async Task<bool> HandleAsync(CreateOrderRequest request, CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(request.CustomerId)) return false;
        if (request.ItemCount <= 0) return false;
        return await CallNextAsync(request, ct);
    }
}

public class AuthHandler : RequestHandlerBase<CreateOrderRequest>
{
    public override async Task<bool> HandleAsync(CreateOrderRequest request, CancellationToken ct)
    {
        // In production: resolve current user and check permissions
        // if (!_authService.IsAuthenticated()) return false;
        return await CallNextAsync(request, ct);
    }
}

public class LoggingHandler : RequestHandlerBase<CreateOrderRequest>
{
    public override async Task<bool> HandleAsync(CreateOrderRequest request, CancellationToken ct)
    {
        // Log request (e.g. _logger.LogInfo("CreateOrder {CustomerId}", request.CustomerId));
        return await CallNextAsync(request, ct);
    }
}

3. Building and running the chain

// Build chain: validation → auth → logging
var validation = new ValidationHandler();
var auth = new AuthHandler();
var logging = new LoggingHandler();
validation.SetNext(auth).SetNext(logging);

var request = new CreateOrderRequest { CustomerId = "C1", ItemCount = 2 };
bool success = await validation.HandleAsync(request);

How this code fits together: (1) The client builds the chain by calling validation.SetNext(auth) and auth.SetNext(logging); SetNext returns the next handler so you can chain calls. (2) The client sends the request only to the first handler: validation.HandleAsync(request). (3) ValidationHandler checks CustomerId and ItemCount; if invalid it returns false and the chain stops. If valid it calls CallNextAsync(request), which invokes auth.HandleAsync(request). (4) AuthHandler performs its check and calls CallNextAsync; LoggingHandler logs and calls CallNextAsync; with no further handler, CallNextAsync returns true. (5) The client gets a single bool indicating success or failure; it does not know which handler failed or how many ran. To add a new step (e.g. rate limit), add a new handler class and insert it in the chain; no other code changes.

When to use Chain of Responsibility: Use when a request must pass through multiple handlers in a fixed or configurable order (middleware, validation → auth → logging, or approval workflows) and you want to add or reorder handlers without changing the sender. Avoid when you have a single handler or a simple linear flow that does not need this flexibility.


Mediator pattern

What it is and when to use it

Mediator defines an object that encapsulates how a set of objects interact, so they don’t reference each other directly. Use it when many components (e.g. chat users, wizard steps, form controls) must coordinate and you want to avoid each one holding references to all the others. The mediator is the single place that knows who is in the system and how messages or actions are routed.

Class structure

Loading diagram…

Class structure explained: The mediator (IChatMediator / ChatRoom) holds references to all colleagues (participants). Colleagues hold only a reference to the mediator, not to each other. When a colleague needs to communicate (e.g. send a message), it calls mediator.Send(message, this). The mediator decides how to route that—e.g. broadcast to all other participants. So the flow is: colleague → mediator → other colleagues. That keeps colleagues decoupled and lets you change routing or add new participant types in one place.

Full working example: Chat room

1. Mediator interface and concrete mediator

namespace MediatorExample;

public interface IChatMediator
{
    void Send(string message, Participant from);
    void Register(Participant participant);
}

public class ChatRoom : IChatMediator
{
    private readonly List<Participant> _participants = new();
    public void Register(Participant p) { _participants.Add(p); }
    public void Send(string message, Participant from)
    {
        foreach (var p in _participants.Where(x => x != from))
            p.Receive(from.Name, message);
    }
}

2. Colleague: Participant

namespace MediatorExample;

public class Participant
{
    public string Name { get; }
    private readonly IChatMediator _mediator;
    public Participant(string name, IChatMediator mediator)
    {
        Name = name;
        _mediator = mediator;
        _mediator.Register(this);
    }
    public void Send(string message) => _mediator.Send(message, this);
    public void Receive(string from, string message) =>
        Console.WriteLine($"{from} -> {Name}: {message}");
}

3. Usage

var room = new ChatRoom();
var alice = new Participant("Alice", room);
var bob = new Participant("Bob", room);
var carol = new Participant("Carol", room);
alice.Send("Hi everyone");  // Bob and Carol receive. Alice does not.
bob.Send("Hello Alice");   // Alice and Carol receive.

How this code fits together: (1) You create one mediator (ChatRoom) and then create participants with that mediator; each participant registers itself in the constructor. (2) When a participant calls Send(message), it delegates to _mediator.Send(message, this). (3) The mediator iterates over all registered participants (excluding the sender) and calls Receive(from, message) on each. Participants never call each other directly; all coordination goes through the mediator.

When to use Mediator: Use when you have many objects that need to coordinate (chat users, wizard steps, form fields that depend on each other) and you want to avoid a web of direct references. Avoid when you only have two or three objects or when the interaction is trivial; the mediator should not become a god object with too much logic.


Memento pattern

What it is and when to use it

Memento captures an object’s internal state so it can be restored later, without exposing that state. Use it for undo/redo, save/restore checkpoints, or any scenario where you need to snapshot and later restore an object’s state. The originator creates and restores mementos; a caretaker (e.g. undo stack) holds mementos but does not read or modify their contents.

Class structure

Loading diagram…

Class structure explained: The originator (Editor) is the object whose state we snapshot. It exposes Save() (or CreateMemento()) which returns a memento—an opaque object holding a copy of the originator’s state. The originator also exposes Restore(memento) to restore its state from a memento. The caretaker (e.g. UndoStack) holds a list of mementos and, on undo, calls originator.Restore(memento). The caretaker never reads or changes the memento’s contents; only the originator knows how to create and interpret mementos. That keeps state encapsulation and makes undo/redo safe.

Full working example: Undoable editor with caretaker

1. Memento and originator (Editor)

namespace MementoExample;

public class EditorMemento
{
    public string Text { get; }
    public EditorMemento(string text) => Text = text;
}

public class Editor
{
    public string Text { get; set; } = "";
    public EditorMemento Save() => new EditorMemento(Text);
    public void Restore(EditorMemento m) => Text = m.Text;
}

2. Caretaker: undo stack

namespace MementoExample;

public class UndoStack
{
    private readonly Editor _editor;
    private readonly Stack<EditorMemento> _history = new();
    public UndoStack(Editor editor) => _editor = editor;

    public void Snapshot()
    {
        _history.Push(_editor.Save());
    }
    public void Undo()
    {
        if (_history.Count == 0) return;
        _history.Pop(); // discard current
        if (_history.Count == 0) return;
        var previous = _history.Pop();
        _editor.Restore(previous);
        _history.Push(previous); // keep it for next undo
    }
}

3. Usage

var editor = new Editor();
var undo = new UndoStack(editor);
editor.Text = "Hello";
undo.Snapshot();
editor.Text = "Hello world";
undo.Snapshot();
editor.Text = "Hello world!";
undo.Snapshot();
undo.Undo(); // back to "Hello world"
undo.Undo(); // back to "Hello"

How this code fits together: (1) The originator (Editor) holds the mutable state (Text) and exposes Save() and Restore(m). Save() returns a memento that only the editor knows how to create and interpret. (2) The caretaker (UndoStack) holds a stack of mementos. Before each user edit you call Snapshot(), which pushes editor.Save() onto the stack. (3) On undo, the caretaker pops the current memento (optional), restores the previous one via editor.Restore(previous), and does not read the memento’s internals. The editor remains the only object that knows the structure of the memento.

When to use Memento: Use when you need undo/redo, checkpoints, or save/restore and want to keep the object’s state encapsulated (callers cannot read or tamper with the snapshot). Avoid when state is trivial or when you need to expose state for other reasons; keep mementos small and immutable.


Iterator pattern

What it is and when to use it

Iterator provides a way to access elements of an aggregate sequentially without exposing its internal representation. Use it when you have a collection or stream of data and you want callers to traverse it one element at a time without knowing whether the data is stored in an array, a list, a tree, or generated on the fly. .NET standardizes this with IEnumerable<T> (the aggregate) and IEnumerator<T> (the iterator).

Class structure

Loading diagram…

Class structure explained: The aggregate (IEnumerable<T> / RangeEnumerable) represents the collection or sequence. It exposes GetEnumerator(), which returns an iterator (IEnumerator<T> / RangeEnumerator). The iterator has MoveNext() (advance and return whether there is a current element) and Current (the element at the current position). The client calls GetEnumerator(), then repeatedly MoveNext() and Current (or uses foreach), and never sees the aggregate’s internal storage. Different aggregates can use different internal structures (array, tree, lazy generation) as long as they return an iterator that obeys the same contract.

Full working example: Custom range collection

1. Aggregate: RangeEnumerable

namespace IteratorExample;

public class RangeEnumerable : IEnumerable<int>
{
    private readonly int _start, _count;
    public RangeEnumerable(int start, int count) { _start = start; _count = count; }
    public IEnumerator<int> GetEnumerator() => new RangeEnumerator(_start, _count);
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}

2. Iterator: RangeEnumerator

namespace IteratorExample;

public class RangeEnumerator : IEnumerator<int>
{
    private readonly int _start, _count;
    private int _current, _index;
    public RangeEnumerator(int start, int count) { _start = start; _count = count; _current = start - 1; _index = -1; }
    public bool MoveNext() { _index++; if (_index >= _count) return false; _current = _start + _index; return true; }
    public int Current => _current;
    object System.Collections.IEnumerator.Current => Current;
    public void Reset() { _index = -1; _current = _start - 1; }
    public void Dispose() { }
}

3. Usage

var range = new RangeEnumerable(10, 3);  // 10, 11, 12
foreach (var n in range)
    Console.WriteLine(n);  // 10, 11, 12

How this code fits together: (1) The aggregate (RangeEnumerable) holds the logical range (_start, _count) and does not expose that as a list or array. (2) GetEnumerator() creates a new iterator (RangeEnumerator) that holds the same range and maintains a current position (_index). (3) The client (or foreach) calls MoveNext() to advance; the iterator computes the next value (_start + _index) and returns it via Current. The client never sees _start or _count; it only sees a sequence of integers. In .NET you can implement the same idea more concisely with yield return inside GetEnumerator().

When to use Iterator: Use when you have an aggregate (custom collection, tree, stream, lazy sequence) and you want callers to traverse it without exposing its internal structure. Use IEnumerable<T> and IEnumerator<T> (or yield return) so your type works with foreach and LINQ. Avoid when a simple array or List<T> is enough and you don’t need to hide representation.


Interpreter pattern

What it is and when to use it

Interpreter defines a grammar for a (small) language and interprets sentences by representing each grammar rule as a class and composing them into a tree. Use it for small DSLs, expression evaluation (e.g. “2 + 3 * 4”), or rule engines where sentences are built from terminal and non-terminal expressions. For large or complex grammars, prefer a parser generator; the Interpreter pattern fits well when the grammar is small and you want the structure in code.

Class structure

Loading diagram…

Class structure explained: Each expression type implements IExpression with an Eval() (or Interpret()) method. Terminal expressions (e.g. NumberExpression) return a value directly. Non-terminal (composite) expressions (e.g. AddExpression, SubtractExpression) hold child expressions and combine their Eval() results. A sentence in the language is represented as a tree of these objects; evaluating the root Eval() traverses the tree and computes the result. A separate parser (not shown) would turn a string like "2 + 3" into this tree.

Full working example: Simple expression evaluator

1. Expression interface and terminal expression

namespace InterpreterExample;

public interface IExpression { decimal Eval(); }

public class NumberExpression : IExpression
{
    private readonly decimal _value;
    public NumberExpression(decimal value) => _value = value;
    public decimal Eval() => _value;
}

2. Binary (composite) expressions

namespace InterpreterExample;

public class AddExpression : IExpression
{
    private readonly IExpression _left, _right;
    public AddExpression(IExpression left, IExpression right) { _left = left; _right = right; }
    public decimal Eval() => _left.Eval() + _right.Eval();
}

public class SubtractExpression : IExpression
{
    private readonly IExpression _left, _right;
    public SubtractExpression(IExpression left, IExpression right) { _left = left; _right = right; }
    public decimal Eval() => _left.Eval() - _right.Eval();
}

3. Building a tree and evaluating (usage)

// Represents: (10 + 3) - 2  =>  13 - 2  =>  11
IExpression expr = new SubtractExpression(
    new AddExpression(new NumberExpression(10), new NumberExpression(3)),
    new NumberExpression(2));
decimal result = expr.Eval();  // 11

How this code fits together: (1) Each grammar rule is a class: numbers are NumberExpression, addition is AddExpression, subtraction is SubtractExpression. (2) A “sentence” is a tree: the root might be SubtractExpression(left, right), and left might be AddExpression(10, 3), etc. (3) Calling Eval() on the root triggers a recursive traversal: composite nodes call Eval() on their children and combine results; terminal nodes return their value. No parser is shown here—in practice a parser would turn a string like "10 + 3 - 2" into this tree. The pattern is: grammar as classes, sentence as tree, interpretation by traversing the tree.

When to use Interpreter: Use when you have a small, well-defined grammar (expressions, simple DSL, rules) and you want to represent it as a tree of objects and interpret by traversing that tree. Avoid for large or ambiguous grammars; use a parser generator and possibly a separate AST plus Visitor for complex languages.


Visitor pattern

What it is and when to use it

Visitor lets you add new operations to a structure of objects without changing their classes. Use it when you have a fixed set of element types (e.g. AST node types) and a growing set of operations (evaluate, print, serialize, type-check). Each operation is implemented in a visitor class; element classes only have an Accept(visitor) method that dispatches to the right VisitX on the visitor (double dispatch). Adding a new operation means adding a new visitor; element types stay unchanged.

Class structure

Loading diagram…

Class structure explained: Each element type (e.g. NumberExpr, AddExpr) implements a common interface with Accept(visitor). When Accept(visitor) is called, the element calls the visitor’s method for its type—e.g. visitor.VisitNumber(this) or visitor.VisitAdd(this)—so the visitor sees the concrete type without the client doing a cast. The visitor interface has one VisitX method per element type. A concrete visitor (e.g. EvalVisitor, PrintVisitor) implements the same operation (evaluate or print) for each type. To add a new operation, add a new visitor class; element classes stay unchanged. The trade-off: adding a new element type requires adding a method to the visitor interface and all existing visitors.

Full working example: AST with Eval and Print visitors

1. Element interface and node types

namespace VisitorExample;

public interface IExpr { T Accept<T>(IExprVisitor<T> v); }

public interface IExprVisitor<T> { T VisitNumber(NumberExpr e); T VisitAdd(AddExpr e); }

public class NumberExpr : IExpr
{
    public int Value { get; set; }
    public T Accept<T>(IExprVisitor<T> v) => v.VisitNumber(this);
}

public class AddExpr : IExpr
{
    public IExpr Left { get; set; }
    public IExpr Right { get; set; }
    public T Accept<T>(IExprVisitor<T> v) => v.VisitAdd(this);
}

2. Eval visitor

namespace VisitorExample;

public class EvalVisitor : IExprVisitor<int>
{
    public int VisitNumber(NumberExpr e) => e.Value;
    public int VisitAdd(AddExpr e) => e.Left.Accept(this) + e.Right.Accept(this);
}

3. Print visitor (second operation)

namespace VisitorExample;

public class PrintVisitor : IExprVisitor<string>
{
    public string VisitNumber(NumberExpr e) => e.Value.ToString();
    public string VisitAdd(AddExpr e) => $"({e.Left.Accept(this)} + {e.Right.Accept(this)})";
}

4. Usage

IExpr expr = new AddExpr { Left = new NumberExpr { Value = 2 }, Right = new NumberExpr { Value = 3 } };
var evalVisitor = new EvalVisitor();
var printVisitor = new PrintVisitor();
int result = expr.Accept(evalVisitor);     // 5
string text = expr.Accept(printVisitor);   // "(2 + 3)"

How this code fits together: (1) Each element (NumberExpr, AddExpr) has Accept(visitor) which calls the visitor’s VisitNumber(this) or VisitAdd(this)—so the visitor receives the concrete type (double dispatch). (2) EvalVisitor implements evaluation: VisitNumber returns the value; VisitAdd returns Left.Accept(this) + Right.Accept(this), so the tree is traversed and values are combined. (3) PrintVisitor implements formatting: each VisitX returns a string; VisitAdd builds "(left + right)" by calling Accept(this) on children. (4) To add another operation (e.g. serialize), add a new visitor class; no change to NumberExpr or AddExpr.

When to use Visitor: Use when you have a stable set of element types and a growing set of operations (evaluate, print, serialize, type-check, etc.) and you want to add operations without changing element classes. Avoid when you frequently add new element types—each new type forces a new method on the visitor interface and on all existing visitors.


Comparison: when to use which

Pattern Use when Avoid when
Chain of Responsibility Middleware, validation pipeline; multiple handlers might handle a request Single handler; simple linear flow
Command CQRS, queues, undo/redo, audit, decoupling request from handler Simple synchronous calls with no queue/undo need
Interpreter DSLs, expression evaluation, small grammars Complex languages; prefer parser generators
Iterator Sequential access to aggregate without exposing structure Simple collections; foreach / IEnumerable suffice
Mediator Many objects must coordinate; chat, wizards, form validation Few objects; direct references are fine
Memento Undo/redo, save/restore; capture state without exposing it No need to restore; state is trivial
Observer One source, many subscribers; event-driven UI or domain events Simple one-to-one callbacks; prefer direct call
State Object behavior varies by internal state; workflows, FSMs Few states and simple branches; if/switch may suffice
Strategy Multiple interchangeable algorithms (pricing, validation, serialization) Only one algorithm; overkill for a single branch
Template Method Same algorithm skeleton, variable steps (ETL, reports) Steps vary wildly; composition (Strategy) might fit better
Visitor Many element types and many operations; AST traversal, double dispatch Few types or one operation; overkill for simple trees

Enterprise and .NET practices

  • Chain of Responsibility: Use for middleware pipelines (e.g. ASP.NET Core middleware); keep handlers focused and pass to next when not handling.
  • Command: Align with CQRS and MediatR for APIs; keep commands small and handlers single-purpose. Use command pipeline behaviors for validation, logging, and transactions.
  • Interpreter: Use for small DSLs or expression trees; for large grammars consider parser generators.
  • Iterator: .NET’s IEnumerable<T> and yield return implement this; use for custom collections.
  • Mediator: Use for chat, wizards, or form coordination; consider MediatR for request/response mediation.
  • Memento: Use for undo/redo or save/restore; keep mementos immutable and small.
  • Observer: Prefer IObservable<T> for composition and cancellation; use events for simple in-process notifications. In distributed systems, use message brokers and keep handlers idempotent.
  • State: Keep transition logic in state classes; consider state machines (e.g. Stateless) for complex workflows.
  • Strategy: Register strategies in DI; use keyed or named services when you have many strategies.
  • Template Method: Use hooks for optional customization; consider composition if subclasses diverge a lot.
  • Visitor: Use for AST traversal or when you have many element types and many operations; add new operations via new visitor classes.

Common pitfalls

  • Chain of Responsibility: Forgetting to call the next handler; handlers that do too much.
  • Command: Putting business logic in the command object instead of the handler; commands that are too large.
  • Interpreter: Using for complex grammars; prefer parser generators for large languages.
  • Iterator: Exposing internal structure; not disposing enumerators when needed.
  • Mediator: Mediator becoming a god object; too much logic in the mediator.
  • Memento: Exposing internal state; mementos that are too large or mutable.
  • Observer: Forgetting to unsubscribe (leaks); not handling errors in observers.
  • State: Mixing state transition logic with business logic; not making transitions explicit.
  • Strategy: Registering multiple strategies without a clear selection mechanism.
  • Template Method: Rigid inheritance; too many levels or steps.
  • Visitor: Adding new element types forces changing all visitors; use when operations change more often than types.


Position & Rationale

I apply Strategy when I have interchangeable algorithms (e.g. pricing, validation) and want to swap them via DI; Observer (or events) when one source notifies many subscribers; Command when I need undo, queue, or audit; State when behaviour depends on internal state and I want to avoid big switch/if chains; Mediator when many objects talk to each other and I want to centralise. I avoid Visitor unless I’m traversing a fixed structure and adding operations without changing types—otherwise it’s often overkill. I also avoid applying a pattern where a simple delegate or interface would do; not every conditional needs a State pattern.


Trade-Offs & Failure Modes

  • What this sacrifices: Extra types and indirection; each pattern adds at least one abstraction. In return you get clearer flow and testability.
  • Where it degrades: When every small behaviour gets a full pattern (e.g. State for two states); or when the team doesn’t recognise the pattern and adds ad hoc logic next to it.
  • How it fails when misapplied: Using Mediator when two objects could just call each other; or Interpreter for something that’s not really a language. Another failure: Observer with no unsubscribe so subscribers leak.
  • Early warning signs: “We have a Strategy but we never swap it”; “our State machine has 50 states and we can’t reason about it”; “nobody knows who handles this request in the chain.”

What Most Guides Miss

Most guides show one example per pattern. The hard part is when to stop: Strategy is great for “plug in another algorithm,” but if you only ever have one implementation, a simple method may be enough. The other gap: Observer and lifecycle—subscribers must unsubscribe or you get leaks and duplicate handling; in .NET, IDisposable and CancellationToken help. Finally: Command and idempotency—if commands are replayed or queued, they must be idempotent or you get duplicate side effects; many guides don’t mention that.


Decision Framework

  • If you have interchangeable algorithms → Strategy (interface + implementations); register in DI.
  • If one source notifies many → Observer or events; ensure subscribers can unsubscribe.
  • If you need undo, queue, or audit → Command (encapsulate request as object); consider idempotency if replayed.
  • If behaviour depends on internal state → State (state classes or state enum + handlers); avoid giant switch when states grow.
  • If many objects interact and coupling is high → Mediator to centralise; don’t use when only two objects talk.
  • If you’re adding operations to a structure without changing types → Visitor; accept the double-dispatch complexity only when the structure is stable.

You can also explore more patterns in the .NET Architecture resource page.

Key Takeaways

  • Use Strategy for swappable algorithms, Observer for one-to-many notification, Command for undo/queue/audit, State for stateful behaviour, Mediator to reduce coupling among many objects.
  • Match the pattern to the problem; don’t force a pattern when a delegate or interface suffices.
  • Observer: ensure unsubscribe to avoid leaks; Command: design for idempotency if replayed or queued.
  • Visitor is for adding operations to a fixed structure; use only when the structure is stable and you have many operations.
  • Recognise the pattern in existing code before refactoring; otherwise you add structure that nobody maintains.

Summary

Behavioral patterns in .NET (Strategy, Observer, Command, State, Template Method, Chain of Responsibility, Mediator, Memento, Iterator, Interpreter, Visitor) each address how objects communicate and delegate work—this article defines all eleven with class diagrams and full working C# examples so you can use them as a final design. Using the wrong pattern or forcing one where a simple delegate or conditional suffices leads to unnecessary complexity; matching the pattern to the problem keeps designs clear and testable. Next, pick one or two patterns that fit your current problem, implement them using the examples above, and combine with DI or events as needed while avoiding the common pitfalls.


Need architectural guidance for real-world .NET platforms? I offer consulting for .NET architecture, API platforms, and enterprise system design.

When I Would Use This Again — and When I Wouldn’t

I would use behavioral patterns again when the problem clearly matches: Strategy for swappable logic, Observer for events, Command for undo/queue, State for stateful flow, Mediator for many interacting objects. I wouldn’t force a pattern when a simple if/switch or delegate is enough—e.g. two states don’t need a full State pattern. I also wouldn’t use Visitor unless I’m adding operations to a stable structure and the team understands double dispatch. Alternative: for small scripts or one-off code, inline conditionals are fine; introduce patterns when you need testability or when the flow is hard to follow.


services
Frequently Asked Questions

Frequently Asked Questions

What is the Observer pattern?

Observer is a behavioral pattern where one object (subject) notifies many dependents (observers) when its state changes. In .NET you use events or IObservable<T> / IObserver<T>.

What is the Strategy pattern?

Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. The context uses an interface (e.g. IPricingStrategy) so you can swap implementations at runtime, often via DI.

What is the Command pattern?

Command encapsulates a request as an object so you can parameterize clients, queue or log requests, and support undo/redo. In .NET, CQRS and MediatR (IRequest / IRequestHandler) implement this pattern.

What is the State pattern?

State lets an object change its behavior when its internal state changes. Each state is a class; the context delegates to the current state and states transition the context to other states. Used for workflows and finite state machines.

What is the Template Method pattern?

Template Method defines the skeleton of an algorithm in a base class and lets subclasses override specific steps. Abstract methods are required overrides; virtual hook methods are optional. Used for ETL, report generation, and fixed flows with variable steps.

When should I use Observer vs events?

Use C# events when you have a small, in-process surface and simple subscription. Use IObservable<T> when you need composition, cancellation (IDisposable), or Rx-style operators. Use message brokers when subscribers are distributed or across processes.

When should I use Strategy vs Template Method?

Strategy uses composition (inject an interface); you can switch algorithms at runtime and avoid inheritance. Template Method uses inheritance and fixes the algorithm structure; subclasses fill in steps. Use Strategy when the whole algorithm varies; use Template Method when the structure is fixed and only steps vary.

How do I register multiple strategies in DI?

Register each strategy (e.g. AddScoped<IPricingStrategy, BulkPricingStrategy>) and inject IEnumerable<IPricingStrategy> or use keyed services (AddKeyedScoped<IPricingStrategy, StandardPricingStrategy>("Standard")) and resolve by key in a factory.

How does Command relate to CQRS?

CQRS separates read (queries) and write (commands). Commands are the write side: each command is an object handled by one handler. MediatR and similar libraries implement this with IRequest<T> and IRequestHandler<T, TResponse>.

Can State pattern have many states?

Yes. Model each state as a class implementing the state interface. The context holds the current state and delegates; states call context.SetState(newState) to transition. For very complex workflows, consider a state machine library (e.g. Stateless).

What are hook methods in Template Method?

Hook methods are virtual methods in the base class with empty or default implementations. Subclasses may override them to customize behavior at specific points in the algorithm without being forced to (unlike abstract methods).

How do I test behavioral patterns?

Observer: Mock observers; test subscribe/notify. Strategy: Test each strategy and context with mock strategy. Command: Test handlers with mock dependencies. State: Test each state and transitions. Template Method: Test concrete subclasses. Chain of Responsibility: Test each handler and chain order. Mediator: Test mediator and participants. Memento: Test save/restore. Iterator: Test enumeration. Interpreter: Test expression evaluation. Visitor: Test visitor for each element type.

What is Chain of Responsibility?

Chain of Responsibility passes a request along a chain of handler objects. Each handler either handles the request or passes it to the next. Use for middleware, validation pipelines, or when multiple handlers might handle a request.

What is Mediator?

Mediator defines an object that encapsulates how a set of objects interact, so they don’t reference each other directly. Use for chat rooms, wizards, or form coordination.

What is Memento?

Memento captures an object’s internal state so it can be restored later, without exposing that state. Use for undo/redo or save/restore.

What is Iterator?

Iterator provides sequential access to aggregate elements without exposing internal structure. .NET uses IEnumerable<T> and IEnumerator<T>; yield return implements it concisely.

What is Interpreter?

Interpreter defines a grammar for a (small) language and interprets sentences. Use for DSLs, expression evaluation, or parsing rules.

What is Visitor?

Visitor lets you add new operations to a structure of objects without changing their classes. Use for double dispatch, AST traversal, or when you have many element types and many operations.

Observer vs message broker?

Observer is in-process: one object notifies many in the same app. A message broker (Service Bus, RabbitMQ) is out-of-process: publishers and subscribers are decoupled and can be in different services. Use Observer inside a service; use a broker across services.

Strategy vs Factory?

Strategy is about swapping behavior (algorithm) at runtime; the context uses the strategy to do work. Factory is creational: it creates objects. You might use a factory to create the right strategy, but they solve different problems.

When is Template Method a bad fit?

When the algorithm structure varies a lot between subclasses, or when you need to swap the entire algorithm at runtime. Prefer Strategy (composition) then. Also avoid deep inheritance trees; keep template methods readable.

When should I use Chain of Responsibility?

Use when a request must pass through multiple handlers in order (middleware, validation → auth → logging, approval workflows) and you want to add or reorder handlers without changing the sender. Avoid when you have a single handler or a simple linear flow.

When should I use Mediator?

Use when many objects must coordinate (chat users, wizard steps, form controls) and you want to avoid each holding references to all the others. Avoid when you only have two or three objects or when the mediator would become a god object.

When should I use Memento?

Use when you need undo/redo, checkpoints, or save/restore and want to keep the object’s state encapsulated. Avoid when state is trivial; keep mementos small and immutable.

When should I use Iterator?

Use when you have a custom collection, tree, or stream and want callers to traverse it without exposing its internal structure. Use IEnumerable<T> and yield return so your type works with foreach and LINQ.

When should I use Interpreter?

Use when you have a small, well-defined grammar (expressions, simple DSL, rules) and want to represent it as a tree of objects and interpret by traversing. For large or ambiguous grammars, use a parser generator instead.

When should I use Visitor?

Use when you have a stable set of element types and a growing set of operations (evaluate, print, serialize, type-check) and want to add operations without changing element classes. Avoid when you frequently add new element types—each new type forces a new method on all visitors.

What if I add a new element type with Visitor?

You must add a VisitX method to the visitor interface and implement it in every existing visitor. That is the trade-off: adding operations is easy (new visitor class); adding element types is costly (change all visitors).

Interpreter vs parser generator?

Interpreter fits small grammars and expression trees; you represent grammar as classes and evaluate by traversing. For large or complex languages, use a parser generator (e.g. ANTLR) and consider a separate AST plus Visitor for analysis.

services
Related Guides & Resources

services
Related services