CQS and commands: make the application understand intent

· 4 min read

engineering architecture #architecture #cqrs #cqs #dotnet #csharp

An UpdateInventoryItem endpoint knows that a record changed. It may not know what the user wanted to do.

That matters when a status change has rules attached to it.

A changed object can hide the request intent

Suppose an inventory item must be deactivated. An update-shaped API might accept this:

UpdateInventoryItem(new InventoryItemDto
{
    Id = itemId,
    Status = InventoryStatus.Deactivated,
    DeactivationReason = reason
});

The handler must infer the action from the changed fields. Is this a temporary hold? Can the item be deactivated while orders are open? Is a reason required for every state transition, or only for this one?

A behavior-focused API names the request instead:

DeactivateInventoryItem(itemId, reason);

The method states what the caller wants, and its parameters show what the request needs. This gives the application one place to validate the reason, check authorization, enforce the transition, and decide whether anything else should happen.

CQS is the rule behind this

Command–Query Separation divides operations into two categories:

This is a rule for methods and APIs. It does not require separate services or databases. A query such as GetInventoryItem should be safe to call repeatedly. A command such as DeactivateInventoryItem asks the system to do work.

The return-value rule makes call sites easier to read. A caller can use a query in a condition, log it, or call it again without wondering whether it changed the system. A command needs more care because it may reject the request or change what later queries return.

Commands should name the business action

Compare these signatures:

SetStatus(itemId, InventoryStatus.Deactivated);
DeactivateInventoryItem(itemId, reason);

SetStatus exposes a field. It tells the caller what the state should be after the update, but it does not explain why that state is valid.

DeactivateInventoryItem exposes an action. The required reason is visible, and the handler has a natural place for rules that belong to deactivation rather than to every possible status change. Microsoft makes the same distinction in its CQRS guidance: commands should represent business tasks rather than low-level data updates.[2]

This does not make every setter a design failure. Changing a preferred locale may be a plain update. The useful question is whether the application needs to understand a specific action or only store a value.

Keep validation and authorization with the command

A command handler can answer the questions that a generic update leaves scattered:

public void DeactivateInventoryItem(Guid itemId, string reason, User currentUser)
{
    if (string.IsNullOrWhiteSpace(reason))
        throw new ValidationException("A deactivation reason is required.");

    authorization.Demand(currentUser, InventoryPermissions.Deactivate);

    var item = inventoryItems.Get(itemId);
    item.Deactivate(reason);

    inventoryItems.Save(item);
}

The UI can disable buttons or explain obvious failures early, but it cannot be the final authority. Another client or a retry can reach the same command. Every caller must pass the command-side rule. The example also assumes that loading, authorization, the state change, and the save run within the appropriate transaction or concurrency policy for the application.

The method is useful because the rule lives beside the behavior it protects.

A command is not an event

The names can look similar, but they mean different things.

Keeping those separate avoids an API where callers announce that something happened before the system has decided whether it can happen.

This point does not require an event bus. In one application, a command can be a method call. If another component needs to be notified later, the successful command can produce an event then.

Make the request visible

DeactivateInventoryItem(itemId, reason) does not create a domain model by itself. It does make the application acknowledge the action it has been asked to perform.

That gives validation, authorization, and business rules a clear home. Command and query code paths can stay separate without requiring separate applications or databases.

Sources

  1. Martin Fowler, "Command Query Separation"
  2. Microsoft Learn, "CQRS pattern"
Share on LinkedIn