CQRS commands don’t actually do anything; they’re just requests that something should happen.
Let’s look at a real command for adding a product to an order:
{
"commandId": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"commandType": "AddProductToOrderCommand",
"timestamp": "2023-10-27T10:00:00Z",
"orderId": "ord-98765",
"productId": "prod-abcde",
"quantity": 2,
"correlationId": "session-xyz789"
}
This JSON object is a command. It’s a self-contained piece of data that describes an intent. It doesn’t contain logic, and it doesn’t directly mutate any state. Instead, it’s handed off to a specialized handler that knows how to interpret and execute it. The commandId is a unique identifier for this specific command instance, useful for tracking. The commandType tells the system what kind of command this is, so the right handler can be invoked. The timestamp indicates when the command was created, and correlationId links it back to a broader operation or user session.
The core purpose of CQRS (Command Query Responsibility Segregation) is to separate the act of changing state (commands) from the act of reading state (queries). This separation allows for independent scaling, optimization, and evolution of these two distinct aspects of an application. Commands represent an intention to change the system’s state, such as "CreateUser," "UpdateProductPrice," or "PlaceOrder." They are imperative – they tell the system what to do. Queries, on the other hand, are declarative – they ask what the current state is, like "GetUserProfile" or "GetOrderHistory."
When a command is dispatched, it travels through a pipeline. First, it undergoes validation. This ensures the command’s data is well-formed and adheres to business rules before any state changes are attempted. For example, the quantity in our AddProductToOrderCommand must be a positive integer. If validation fails, the command is rejected, and an appropriate error is returned to the originator. This prevents invalid data from ever reaching the core business logic.
After validation, the command is routed to its specific command handler. This handler is a piece of code responsible for fulfilling the command’s intent. For AddProductToOrderCommand, the handler would locate the order identified by orderId, verify that productId exists, and then add the specified quantity of that product to the order. This process often involves interacting with an aggregate root – an object that encapsulates a cluster of domain objects and enforces invariants. The handler loads the order aggregate, calls a method on it (e.g., order.addProduct(productId, quantity)), and then saves the updated aggregate.
The dispatching mechanism itself can vary. It might be a simple in-process call if the command and handler are in the same service. More commonly in distributed systems, commands are placed onto a message queue or bus (like RabbitMQ, Kafka, or Azure Service Bus). This decoupling allows for asynchronous processing, retries, and scalability. A dedicated message consumer then picks up the command and invokes the appropriate handler. The correlationId is crucial here for tracing requests across asynchronous boundaries.
Consider the internal workings of the AddProductToOrderCommand handler. It wouldn’t just blindly update a database. It would first load the Order aggregate. If the order doesn’t exist, it might throw an OrderNotFoundException. If the product doesn’t exist, it might throw a ProductNotFoundException. If the order is already in a "Shipped" state, it might throw an OrderAlreadyShippedException. These are domain-specific exceptions that the handler catches and translates into meaningful errors for the caller. The handler ensures that the business rules of the order aggregate are always respected.
The key takeaway is that commands are immutable facts about something that should happen. They are not operations. This distinction is vital for building robust, scalable systems. For instance, a command handler might publish domain events after successfully processing a command. For our example, after successfully adding a product, the Order aggregate might emit an ProductAddedToOrderEvent. This event would then be consumed by other parts of the system, such as an inventory service or a notification service, further decoupling responsibilities.
The most challenging part for newcomers is often understanding that the command itself doesn’t contain the logic. It’s just data. The system’s intelligence lies in the validators and handlers that process this data. A common pitfall is embedding business logic directly within the command object, which defeats the purpose of CQRS and makes the system harder to manage. The command is a message; the handlers are the actors that respond to those messages.
The next logical step after mastering commands is understanding how queries are structured and optimized in a CQRS architecture.