BrandGhost
When to Use Command Pattern in C#: Decision Guide with Examples

When to Use Command Pattern in C#: Decision Guide with Examples

When to Use Command Pattern in C#: Decision Guide with Examples

Every application eventually needs to do more than just call a method and forget about it. Maybe you need to undo what the user just did. Maybe you need to queue operations for later execution, or log every action for an audit trail. The when to use command pattern in C# question surfaces the moment you realize that simply invoking a method directly is no longer enough -- you need to treat operations as first-class objects that can be stored, queued, reversed, or replayed.

This article gives you a structured decision framework for recognizing when the command pattern is the right tool and -- just as importantly -- when it adds complexity without payoff. We'll walk through real code examples covering undo/redo systems, command queuing, request logging, transactional operations, macro commands, and CQRS architecture. You'll also learn how the command pattern compares to alternatives like direct method calls and the strategy pattern, so you can make informed decisions instead of reaching for patterns out of habit.

What the Command Pattern Actually Does

The command pattern encapsulates a request as an object. That single idea unlocks several capabilities that direct method calls can't provide. Instead of calling document.Bold() directly, you create a BoldCommand object that knows how to execute the operation and -- optionally -- how to undo it. The caller doesn't need to know what the command does internally. It just calls Execute().

This separation between "what to do" and "when to do it" is the core value. When a request is an object, you can pass it around, store it in a collection, serialize it, send it across a network, or replay it later. A method call is fire-and-forget. A command object is a reified action that persists beyond the moment of invocation. Unlike the adapter pattern which translates between interfaces, the command pattern wraps behavior itself -- turning actions into data.

The standard structure involves four roles. A command interface declares Execute() and optionally Undo(). Concrete commands implement that interface and hold references to the receiver -- the object that actually performs the work. An invoker stores and triggers commands without knowing what they do. And the receiver contains the business logic the command delegates to.

Signs You Need the Command Pattern in C#

Not every operation needs to be wrapped in a command object. But certain patterns in your codebase are strong signals that the command pattern earns its weight.

You Need Undo/Redo Functionality

This is the classic use case. If users need to reverse their actions -- text editors, drawing applications, configuration tools -- the command pattern gives you a clean mechanism. Each command knows how to execute and undo itself. You maintain a history stack of executed commands and pop from it to undo. A redo stack holds undone commands for re-execution. Without this pattern, undo logic gets scattered across your application and becomes nearly impossible to maintain.

You Need to Queue or Schedule Operations

When operations can't run immediately -- because a resource is busy, a service is temporarily unavailable, or work needs to be batched -- command objects let you queue requests and process them later. The invoker adds commands to a queue. A processor pulls them off and executes them when conditions are right. This decouples the timing of a request from its execution.

You Need Request Logging or Auditing

If your application must record every action for compliance, debugging, or analytics, commands give you a natural logging boundary. Each command object represents a discrete action with all its parameters. You can serialize commands to a log, replay them to reconstruct state, or inspect them during debugging. Direct method calls scatter this information across call sites.

You Need Transactional Behavior

When a sequence of operations must either all succeed or all roll back, commands provide a framework for transactional execution. Each command implements Undo(), and a transaction coordinator can reverse completed commands if any step fails. This is cleaner than scattering try-catch-rollback logic across your service layer.

You Want to Compose Macro Commands

Sometimes you need to bundle multiple operations into a single action. A "deploy" operation might involve stopping a service, copying files, running migrations, and restarting the service. A macro command wraps a list of sub-commands and executes them sequentially. This composition works naturally with the composite pattern -- a macro command is itself a command that contains other commands.

You're Building a CQRS Architecture

Command Query Responsibility Segregation explicitly separates read operations from write operations. The "C" in CQRS is literally commands. Each write operation is represented as a command object with a dedicated handler. This architectural style benefits from the command pattern because it enforces a clear boundary between mutations and queries, making the system easier to scale, test, and reason about.

When NOT to Use the Command Pattern

Knowing when to use the command pattern in C# is only half the picture. Reaching for it when simpler approaches work adds layers of indirection that make your code harder to read and maintain.

Simple Direct Method Calls Are Sufficient

If you're calling a method, the caller knows exactly what it's calling, and you don't need undo, queuing, logging, or any of the capabilities that commands enable -- just call the method. Wrapping _repository.Save(entity) in a SaveEntityCommand class that does nothing but delegate to _repository.Save(entity) is ceremony without benefit.

Ask yourself: will this command object ever be stored, queued, undone, or inspected independently of its execution? If the answer is no, a direct method call is the better choice.

No Undo Is Needed

The command pattern's overhead is most justified when you need reversibility. If your operations are fire-and-forget -- HTTP request handlers, background job processors, event consumers -- the undo infrastructure goes unused. You're paying the complexity cost for a capability you'll never exercise.

Over-Engineering Risk Is High

In smaller applications or CRUD-heavy services, the command pattern can easily become over-engineering. Every operation gets its own command class, its own handler, and its own registration in the dependency injection container. A service with twenty endpoints can balloon into sixty or more classes. If you're not gaining undo, queuing, logging, or composition from this structure, you've traded simplicity for architectural purity.

The Operation Has No State Worth Capturing

Commands are valuable because they reify an operation -- they make an action into a thing with parameters, state, and identity. If the operation is stateless or trivially parameterized, the command object doesn't capture anything meaningful. A ClearCacheCommand with no parameters and no undo is just a function call wearing a costume.

Decision Framework for the Command Pattern in C#

When evaluating whether the command pattern fits your situation, walk through these questions. If most of them get a "yes," the pattern is likely worth the investment.

Do you need to decouple "what" from "when"? If operations need to be stored, queued, scheduled, or deferred, the command pattern gives you that separation. Direct method calls bind invocation to execution. Commands let you separate them.

Do you need reversibility? Undo/redo is the command pattern's signature capability. If your application requires users to reverse actions -- or if you need transactional rollback across multiple steps -- commands with Undo() methods provide a clean, consistent mechanism.

Do you need an operation log? When every action must be recorded for auditing, replay, or debugging, command objects serve as natural log entries. Each command captures the operation type and its parameters in a self-describing object.

Is the operation complex enough to justify encapsulation? Commands add a class per operation. If the operation involves multiple parameters, conditional logic, receiver interactions, and potential reversal, encapsulation pays off. If it's a single method call with no state, the overhead isn't worth it.

Are you building an architecture that demands it? CQRS, event sourcing, and certain plugin architectures are designed around command objects. In these contexts, the pattern isn't optional -- it's foundational.

Scenario 1: Undo/Redo in a Text Editor

The most intuitive application of the command pattern is undo/redo. Here's a text editor that supports undoable operations:

public interface ICommand
{
    void Execute();
    void Undo();
}

public sealed class TextDocument
{
    private readonly StringBuilder _content = new();

    public string Content => _content.ToString();

    public void InsertAt(int position, string text)
    {
        _content.Insert(position, text);
    }

    public void DeleteAt(int position, int length)
    {
        _content.Remove(position, length);
    }
}

Each editing operation becomes a command:

public sealed class InsertTextCommand : ICommand
{
    private readonly TextDocument _document;
    private readonly int _position;
    private readonly string _text;

    public InsertTextCommand(
        TextDocument document,
        int position,
        string text)
    {
        _document = document;
        _position = position;
        _text = text;
    }

    public void Execute()
    {
        _document.InsertAt(_position, _text);
    }

    public void Undo()
    {
        _document.DeleteAt(_position, _text.Length);
    }
}

The invoker manages the history:

public sealed class CommandHistory
{
    private readonly Stack<ICommand> _undoStack = new();
    private readonly Stack<ICommand> _redoStack = new();

    public void ExecuteCommand(ICommand command)
    {
        command.Execute();
        _undoStack.Push(command);
        _redoStack.Clear();
    }

    public void Undo()
    {
        if (_undoStack.Count == 0)
        {
            return;
        }

        var command = _undoStack.Pop();
        command.Undo();
        _redoStack.Push(command);
    }

    public void Redo()
    {
        if (_redoStack.Count == 0)
        {
            return;
        }

        var command = _redoStack.Pop();
        command.Execute();
        _undoStack.Push(command);
    }
}

Every action is recorded. Every action can be reversed. The TextDocument doesn't know about undo logic. The CommandHistory doesn't know about text editing. Each piece has a single responsibility, and the command pattern ties them together.

Scenario 2: Command Queuing for Background Processing

When operations can't execute immediately, commands let you queue them for later processing. This is common in systems that need to throttle throughput, batch operations, or retry on failure:

public interface IQueueableCommand
{
    string Description { get; }
    Task ExecuteAsync(CancellationToken cancellationToken);
}

public sealed class SendEmailCommand : IQueueableCommand
{
    private readonly string _recipient;
    private readonly string _subject;
    private readonly string _body;

    public SendEmailCommand(
        string recipient,
        string subject,
        string body)
    {
        _recipient = recipient;
        _subject = subject;
        _body = body;
    }

    public string Description =>
        $"Send email to {_recipient}: {_subject}";

    public async Task ExecuteAsync(
        CancellationToken cancellationToken)
    {
        Console.WriteLine(
            $"Sending email to {_recipient}: {_subject}");

        await Task.Delay(100, cancellationToken);
    }
}

A command processor pulls from the queue and executes:

public sealed class CommandQueue
{
    private readonly Queue<IQueueableCommand> _pending = new();
    private readonly List<IQueueableCommand> _completed = new();

    public void Enqueue(IQueueableCommand command)
    {
        _pending.Enqueue(command);
        Console.WriteLine($"Queued: {command.Description}");
    }

    public async Task ProcessAllAsync(
        CancellationToken cancellationToken)
    {
        while (_pending.Count > 0)
        {
            var command = _pending.Dequeue();
            Console.WriteLine(
                $"Processing: {command.Description}");

            await command.ExecuteAsync(cancellationToken);

            _completed.Add(command);
        }

        Console.WriteLine(
            $"Processed {_completed.Count} commands.");
    }
}

The queue doesn't care what the commands do. It just stores them and processes them in order. You could add retry logic, priority ordering, or parallel execution without changing the command classes themselves.

Scenario 3: Audit Logging with Command Objects

When every action must be recorded for compliance or debugging, command objects serve as natural audit entries. Each command captures what was done and can be logged before or after execution:

public interface IAuditableCommand
{
    string ActionName { get; }
    string UserId { get; }
    Dictionary<string, string> Parameters { get; }

    void Execute();
}

public sealed class TransferFundsCommand : IAuditableCommand
{
    private readonly string _fromAccount;
    private readonly string _toAccount;
    private readonly decimal _amount;

    public TransferFundsCommand(
        string userId,
        string fromAccount,
        string toAccount,
        decimal amount)
    {
        UserId = userId;
        _fromAccount = fromAccount;
        _toAccount = toAccount;
        _amount = amount;
    }

    public string ActionName => "TransferFunds";
    public string UserId { get; }

    public Dictionary<string, string> Parameters => new()
    {
        ["FromAccount"] = _fromAccount,
        ["ToAccount"] = _toAccount,
        ["Amount"] = _amount.ToString("F2")
    };

    public void Execute()
    {
        Console.WriteLine(
            $"Transferring ${_amount} " +
            $"from {_fromAccount} to {_toAccount}");
    }
}

An auditing invoker wraps execution with logging:

public sealed class AuditingInvoker
{
    private readonly List<AuditEntry> _auditLog = new();

    public void Execute(IAuditableCommand command)
    {
        var entry = new AuditEntry(
            command.ActionName,
            command.UserId,
            command.Parameters,
            DateTimeOffset.UtcNow);

        try
        {
            command.Execute();
            _auditLog.Add(entry with { Success = true });
        }
        catch (Exception ex)
        {
            _auditLog.Add(entry with
            {
                Success = false,
                ErrorMessage = ex.Message
            });

            throw;
        }
    }

    public IReadOnlyList<AuditEntry> GetAuditLog() =>
        _auditLog.AsReadOnly();
}

public record AuditEntry(
    string Action,
    string UserId,
    Dictionary<string, string> Parameters,
    DateTimeOffset Timestamp,
    bool Success = false,
    string? ErrorMessage = null);

Every command self-describes its action, user, and parameters. The invoker captures success or failure. You get a complete audit trail without scattering logging code across your service layer. This approach pairs well with inversion of control -- the invoker depends on the IAuditableCommand abstraction, not on any specific operation.

Scenario 4: Transactional Macro Commands

When multiple operations must succeed or fail together, macro commands let you compose individual commands into a transaction. If any step fails, previously executed commands are undone in reverse order:

public sealed class TransactionalMacroCommand : ICommand
{
    private readonly List<ICommand> _commands;
    private readonly Stack<ICommand> _executed = new();

    public TransactionalMacroCommand(
        IEnumerable<ICommand> commands)
    {
        _commands = commands.ToList();
    }

    public void Execute()
    {
        try
        {
            foreach (var command in _commands)
            {
                command.Execute();
                _executed.Push(command);
            }
        }
        catch
        {
            Undo();
            throw;
        }
    }

    public void Undo()
    {
        while (_executed.Count > 0)
        {
            var command = _executed.Pop();
            command.Undo();
        }
    }
}

You can compose concrete commands into a deployment transaction:

public sealed class StopServiceCommand : ICommand
{
    private readonly string _serviceName;

    public StopServiceCommand(string serviceName)
    {
        _serviceName = serviceName;
    }

    public void Execute()
    {
        Console.WriteLine($"Stopping service: {_serviceName}");
    }

    public void Undo()
    {
        Console.WriteLine($"Starting service: {_serviceName}");
    }
}

public sealed class RunMigrationCommand : ICommand
{
    private readonly string _migrationId;

    public RunMigrationCommand(string migrationId)
    {
        _migrationId = migrationId;
    }

    public void Execute()
    {
        Console.WriteLine(
            $"Running migration: {_migrationId}");
    }

    public void Undo()
    {
        Console.WriteLine(
            $"Rolling back migration: {_migrationId}");
    }
}

// Usage
var deploy = new TransactionalMacroCommand(new ICommand[]
{
    new StopServiceCommand("OrderApi"),
    new RunMigrationCommand("2024-03-add-status-column"),
    new StopServiceCommand("InventoryApi")
});

deploy.Execute();

If the second StopServiceCommand throws, the migration and the first stop are undone in reverse order. The macro command itself implements ICommand, so it can be nested inside other macros -- a natural fit for the composite approach. This pattern shines in deployment pipelines, batch imports, and any workflow where partial completion is worse than no execution at all.

Command Pattern vs Alternatives: When to Choose What

Understanding when to use the command pattern in C# means knowing how it compares to similar approaches. Here's a breakdown:

Criteria Command Pattern Strategy Pattern Observer Pattern Direct Method Call
Primary purpose Encapsulate a request as an object Swap algorithms at runtime Notify subscribers of state changes Invoke behavior immediately
Supports undo Yes -- built-in No No No
Supports queuing Yes -- commands are storable Not typically Events can be queued No
Supports logging Yes -- commands self-describe No Events can be logged Manual logging required
Supports composition Yes -- macro commands No Chainable No
Overhead One class per operation One class per algorithm Publisher + subscriber wiring Minimal

The command pattern and the strategy pattern both encapsulate behavior behind an interface, but they serve different purposes. A strategy replaces how something is done. A command captures what is done so it can be deferred, reversed, or replayed. You might use both together -- a strategy to select which pricing algorithm to apply, and a command to make that pricing calculation undoable.

The observer pattern and the command pattern can look similar when commands trigger notifications. The key difference is direction. Commands flow from invoker to receiver -- "do this thing." Observers flow from subject to subscribers -- "this thing happened." In event-driven architectures, a command might execute an operation, and the receiver might raise an event that observers react to. They're complementary, not competing.

Red Flags: When the Command Pattern Is Overkill

Even when the command pattern seems applicable, watch for these signs that you're adding more complexity than value.

Every command is a thin wrapper around a single method call. If your SaveUserCommand just calls _repository.Save(user) with no undo, no logging, and no queuing, you've added a class for no reason. The command should be doing something that a direct call can't.

You're creating command classes for CRUD operations without CQRS. In a standard web API, wrapping each endpoint's logic in a command class can be overkill unless you're deliberately adopting CQRS. A service class with methods is simpler and more readable for straightforward create-read-update-delete operations.

The undo logic is more complex than the execute logic. Some operations are easy to perform but extremely difficult to reverse. Deleting a database record is one line. Restoring it -- with all its relationships, foreign keys, and cascaded side effects -- can be a nightmare. If your undo implementation is fragile or incomplete, the command pattern gives a false sense of reversibility.

You're using it purely for organizational purposes. The command pattern isn't a code organization tool. If you want to group related operations, namespaces and service classes do that. Commands exist to enable specific capabilities like undo, queuing, and logging. Using them as a filing system adds indirection without enabling anything new.

Frequently Asked Questions

What is the command pattern in C# and when should I use it?

The command pattern is a behavioral design pattern that encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations. Use it when you need to decouple the object that invokes an operation from the object that performs it -- especially when you need undo/redo, command queuing, audit logging, or transactional behavior. If you're just calling a method and moving on, direct invocation is simpler.

How does the command pattern support undo and redo in C#?

Each command class implements both Execute() and Undo() methods. When a command executes, it gets pushed onto a history stack. To undo, you pop the most recent command and call Undo(). For redo, undone commands move to a redo stack and can be re-executed. The command object stores enough state to reverse its own operation -- for example, an InsertTextCommand remembers the position and text it inserted so it can delete exactly that text during undo.

Can I combine the command pattern with dependency injection in C#?

Yes. Commands and their handlers integrate naturally with dependency injection. In a CQRS setup, you register command handlers in your DI container and resolve them at runtime based on the command type. The invoker doesn't need to know which handler processes which command -- the container handles that wiring. This keeps your command infrastructure loosely coupled and testable.

What is the difference between the command pattern and the strategy pattern?

The strategy pattern lets you swap algorithms at runtime -- how something is done changes. The command pattern encapsulates what is done as an object so it can be stored, queued, undone, or replayed. Strategies are about behavioral variation. Commands are about operational reification. They solve different problems but can work together -- a command might use a strategy internally to decide how to perform its operation.

Is the command pattern the same as CQRS?

No, but CQRS builds on the command pattern. CQRS separates read models from write models at an architectural level. The command pattern provides the mechanism for representing write operations as objects. You can use the command pattern without CQRS -- for undo/redo in a desktop application, for example. And CQRS uses commands as its write-side building block but also involves separate read models, potentially different data stores, and event sourcing. The command pattern is a design pattern. CQRS is an architectural pattern.

When should I use the observer pattern instead of the command pattern?

Use the observer pattern when you need to notify multiple subscribers that something happened -- "this event occurred, react as you see fit." Use the command pattern when you need to encapsulate and control an operation -- "do this thing, and maybe undo it later." Observers are reactive and broadcast-oriented. Commands are imperative and targeted. In many systems, they work together: a command executes an action, and the receiver publishes an event that observers handle.

Does the command pattern work well with async operations in C#?

Yes. Define your command interface with Task ExecuteAsync(CancellationToken cancellationToken) instead of void Execute(). Async commands work naturally with queuing, background processing, and retry logic. The command object still encapsulates the operation -- the only difference is that execution is awaitable. This fits well with modern C# patterns where I/O-bound operations like database writes, HTTP calls, and message publishing are inherently asynchronous.

Wrapping Up the Command Pattern Decision Guide

Deciding when to use the command pattern in C# comes down to whether you need to treat operations as objects. If you need undo/redo, command queuing, audit logging, transactional rollback, or macro composition, the pattern gives you a clean, extensible way to structure those capabilities. Each command encapsulates its operation, its parameters, and optionally its reversal logic in a single cohesive class.

The decision framework is practical. Check whether you need to decouple invocation from execution. Ask if reversibility, logging, or queuing are requirements. Confirm that the operation is complex enough to justify its own class. And watch for the red flags -- thin wrappers around single method calls, unnecessary CRUD command classes, and undo logic that's more fragile than the operation itself.

Start with direct method calls. They're simple, readable, and sufficient for the vast majority of operations. When you notice that you need to store, queue, reverse, or log those operations -- that's your signal to reach for the command pattern. Keep commands focused on a single operation, implement undo only when it's genuinely needed, and let the pattern earn its place through the capabilities it enables rather than applying it as a default architectural choice.

Command Pattern in C# - What You Need to Implement It

Organize your code with the Command Pattern in C#! Learn what the Command Pattern in C# is and the design principles it follows. Understand the pros and cons!

Command Design Pattern in C#: Complete Guide with Examples

Master the command design pattern in C# with practical examples showing encapsulated requests, undo/redo functionality, and command queuing.

When to Use Composite Pattern in C#: Decision Guide with Examples

Discover when to use composite pattern in C# with real decision criteria, use case examples, and guidance on when simpler alternatives work better.

An error has occurred. This application may no longer respond until reloaded. Reload