// Design Patterns · 14
Command
Turn a request into an object you can queue, log or undo.
How it fits together
- OrdersController creates CancelOrder (new)
- OrdersController uses / calls CommandHandler (Handle(cmd))
- CancelOrderHandler implements CommandHandler
- CancelOrderHandler uses / calls CancelOrder (reads)
Spot it
- Need undo, redo or replay of actions
- Work must be queued or retried later
- Controllers full of business logic
It costs
A class per action and an indirection between the caller and the work.
In code
public sealed record CancelOrder(Guid OrderId, string Reason);public interface ICommandHandler<in TCommand>{ Task HandleAsync(TCommand command, CancellationToken ct);}public sealed class CancelOrderHandler(IOrderRepository orders) : ICommandHandler<CancelOrder>{ public async Task HandleAsync(CancelOrder command, CancellationToken ct) { var order = await orders.LoadAsync(command.OrderId, ct) ?? throw new KeyNotFoundException($"Order {command.OrderId} not found."); order.Cancel(command.Reason); await orders.SaveAsync(order, ct); }}public sealed class OrdersController(ICommandHandler<CancelOrder> cancelOrder){ public Task Cancel(Guid id, string reason, CancellationToken ct) => cancelOrder.HandleAsync(new CancelOrder(id, reason), ct);}Go deeperThe full explanation and what to ask for · 1 min readExpandCollapse
A command packages a request and its parameters as an object. The code that triggers it no longer needs to know who does the work or when, so commands can be queued, logged, retried or undone.
Handlers that are one-line pass-throughs add files without adding meaning. A generic dispatcher that routes everything makes it hard to see what a request actually runs.
Immutable value types make good commands, with equality by value. A message on a queue is a serialised command, which is why handlers should be idempotent. CQRS builds on the split between commands that change state and queries that read it.
Ask for it
- command object
- handler
- invoker / receiver
- undo / redo
- command queue
- CQRS
Move the logic in OrdersController.Cancel into a CancelOrder command type and a CancelOrderHandler that handles it, registered in the DI container. The controller should only map the request and call the handler.