Command

Command

A restaurant order ticket is a Command. The waiter writes down what you want (the request), walks it to the kitchen (the invoker), and the chef prepares it (the receiver). The ticket can be queued behind other orders, prioritized for VIPs, cancelled before cooking starts, or saved in a log for the end-of-night audit. The waiter doesn’t cook; the chef doesn’t take orders. The ticket decouples who requests from who executes.

The Command pattern encapsulates a request as an object, bundling the action, its parameters, and the receiver into one unit. An invoker calls Execute() without knowing what the command does; the command knows how to do it and — critically — how to Undo() it. Because commands are objects, they can be serialized, queued, logged, retried, and replayed. In an e-commerce system, PlaceOrderCommand, CancelOrderCommand, and RefundOrderCommand each carry their context and can be pushed onto a command history for full undo support.

sequenceDiagram
    participant Client
    participant Invoker as Command Queue
    participant Command as PlaceOrderCommand
    participant Receiver as OrderService
    Client->>Command: Create with order data
    Client->>Invoker: Submit command
    Invoker->>Command: Execute
    Command->>Receiver: Process order
    Receiver-->>Command: Result
    Note over Invoker: Can also Undo or Retry
Command vs Strategy

Command encapsulates a request with all its data — what to do, when, and with what context. Strategy encapsulates an algorithm — how to do something. Command is about WHAT and WHEN; Strategy is about HOW. A PlaceOrderCommand carries the order data and knows how to place it. A ShippingCostStrategy knows how to calculate cost but doesn't carry the order.

Problem

OrderService has PlaceOrder, CancelOrder, RefundOrder methods — no undo, no queuing, no audit trail:

public class OrderService
{
    // ⚠️ No undo — once placed, can't roll back without a separate cancel call
    public async Task PlaceOrderAsync(Order order)
    {
        await _repository.SaveAsync(order);
        await _inventory.ReserveAsync(order.Items);
        await _payment.ChargeAsync(order.Total, order.Customer.PaymentMethod);
    }

    // ⚠️ No record of who cancelled, when, or why
    public async Task CancelOrderAsync(Guid orderId)
    {
        var order = await _repository.GetAsync(orderId);
        order.Status = OrderStatus.Cancelled;
        await _repository.UpdateAsync(order);
        await _inventory.ReleaseAsync(order.Items);
        await _payment.RefundAsync(order.PaymentTransactionId, order.Total);
    }

    // ⚠️ Retry logic scattered — if refund fails, caller must retry manually
    public async Task RefundOrderAsync(Guid orderId, decimal amount) { /* ... */ }
}

Here's what breaks when requirements change: adding an "undo last action" feature for customer service agents requires tracking what was done and how to reverse it — there's no structure for that.

Solution

Each operation becomes a command object with ExecuteAsync() and UndoAsync():

// Command interface
public interface IOrderCommand
{
    Task ExecuteAsync();
    Task UndoAsync();
    string Description { get; } // for audit trail
}

// Concrete commands
public class PlaceOrderCommand(
    Order order,
    IOrderRepository repository,
    IInventoryService inventory,
    IPaymentService payment) : IOrderCommand
{
    private string? _transactionId;

    public string Description => $"Place order #{order.Id} for {order.Total:C}";

    public async Task ExecuteAsync()
    {
        await repository.SaveAsync(order);
        await inventory.ReserveAsync(order.Items);
        _transactionId = await payment.ChargeAsync(order.Total, order.Customer.PaymentMethod);
        order.Status = OrderStatus.Paid;
    }

    public async Task UndoAsync()
    {
        // ✅ Undo knows exactly what to reverse
        if (_transactionId is not null)
            await payment.RefundAsync(_transactionId, order.Total);
        await inventory.ReleaseAsync(order.Items);
        order.Status = OrderStatus.Cancelled;
        await repository.UpdateAsync(order);
    }
}

public class CancelOrderCommand(
    Guid orderId,
    string reason,
    IOrderRepository repository,
    IInventoryService inventory,
    IPaymentService payment) : IOrderCommand
{
    private Order? _cancelledOrder;

    public string Description => $"Cancel order #{orderId}: {reason}";

    public async Task ExecuteAsync()
    {
        _cancelledOrder = await repository.GetAsync(orderId);
        _cancelledOrder.Status = OrderStatus.Cancelled;
        _cancelledOrder.CancellationReason = reason;
        await repository.UpdateAsync(_cancelledOrder);
        await inventory.ReleaseAsync(_cancelledOrder.Items);
        await payment.RefundAsync(_cancelledOrder.PaymentTransactionId, _cancelledOrder.Total);
    }

    public async Task UndoAsync()
    {
        if (_cancelledOrder is null) return;
        _cancelledOrder.Status = OrderStatus.Paid;
        await repository.UpdateAsync(_cancelledOrder);
        await inventory.ReserveAsync(_cancelledOrder.Items);
        // Note: re-charging is not always possible — undo may be limited
    }
}

// Invoker — executes commands and maintains history for undo
public class OrderCommandInvoker
{
    private readonly Stack<IOrderCommand> _history = new();
    private readonly ICommandAuditLog _auditLog;

    public OrderCommandInvoker(ICommandAuditLog auditLog) => _auditLog = auditLog;

    public async Task ExecuteAsync(IOrderCommand command)
    {
        await command.ExecuteAsync();
        _history.Push(command);
        await _auditLog.RecordAsync(command.Description, DateTime.UtcNow); // ✅ automatic audit trail
    }

    public async Task UndoLastAsync()
    {
        if (_history.TryPop(out var command))
        {
            await command.UndoAsync();
            await _auditLog.RecordAsync($"UNDO: {command.Description}", DateTime.UtcNow);
        }
    }
}

// Usage
var placeCommand = new PlaceOrderCommand(order, repository, inventory, payment);
await invoker.ExecuteAsync(placeCommand);

// Customer service agent undoes the last action
await invoker.UndoLastAsync();

Adding a PartialRefundCommand now means one new class — the invoker and audit trail work automatically.

You Already Use This

ICommand (WPF/MAUI)Execute(parameter) and CanExecute(parameter) are the Command pattern. UI buttons bind to commands; the command encapsulates the action and its preconditions. The button doesn't know what the command does.

MediatR IRequest<T> + handler — MediatR is a Command pattern implementation. PlaceOrderCommand : IRequest<OrderResult> is the command; PlaceOrderCommandHandler is the receiver. The mediator is the invoker.

IDbCommand / SqlCommandSqlCommand encapsulates a SQL statement, its parameters, and the connection. ExecuteNonQueryAsync() is Execute(). The command can be prepared, parameterized, and reused.

Action<T> / Func<T> delegates — a delegate is a lightweight command. Task.Run(() => ProcessOrder(order)) queues a command for execution on the thread pool.

Questions

References


Whats next