CQRS

2 min read
On trails:EDAPATstep 5/7

CQRS, Command Query Responsibility Segregation, uses one model to change data and a different one to read it. A command changes state and goes through the business rules. A query returns data in the shape a screen or report wants and changes nothing. In an event-driven system the two models are usually joined by events: the write side publishes them, and the read side consumes them to keep its own copy up to date.

In code

In the online shop, PlaceOrder is a command and GetOrderSummary is a query. The write side loads or creates an Order, applies the rules and saves it. The read side never touches the Order. It reads a flat OrderSummary that was built for display.

commands-and-queries.csC#
public record PlaceOrder(Guid OrderId, Guid CustomerId, IReadOnlyList<OrderLine> Lines);public record GetOrderSummary(Guid OrderId);public record OrderSummary(Guid OrderId, string Status, decimal Total, int ItemCount); // Write side: validates, changes state, records the event.public class PlaceOrderHandler(IOrderRepository orders){    public async Task Handle(PlaceOrder command, CancellationToken ct)    {        var order = Order.Place(command.OrderId, command.CustomerId, command.Lines);        await orders.SaveAsync(order, ct); // also writes OrderPlaced to the outbox    }} // Read side: no domain logic, just a lookup in the read model.public class GetOrderSummaryHandler(IReadDb db){    public Task<OrderSummary?> Handle(GetOrderSummary query, CancellationToken ct) =>        db.OrderSummaries.FindAsync(query.OrderId, ct);} // Keeps the read model up to date from the events.public class OrderSummaryProjection(IReadDb db){    public Task On(OrderPlaced evt, CancellationToken ct) =>        db.OrderSummaries.UpsertAsync(            new OrderSummary(evt.OrderId, "Placed", evt.Total, evt.Lines.Count), ct);}

The command and query records are the two sides of the split. The handler for each does one job. The projection at the bottom is what joins them: when OrderPlaced arrives, it writes or updates the summary. An upsert is safe to repeat, which matters because the same event can arrive twice (see idempotent consumersIdempotent consumersA consumer that gives the same result whether a message arrives once or five times.Opens in a new tab). The save and the event must be recorded together, which is what the outboxOutbox and inboxWrite the event in the same transaction as the data, publish it from there, and deduplicate on the way in.Opens in a new tab is for. IReadDb stands for whatever store holds the read model.

It does not need event sourcing

CQRS and event sourcingEvent sourcingStore the events that happened, and work out the current state from them.Opens in a new tab are often mentioned together, but they are separate. Fowler writes that strictly, CQRS is not about events. The simplest form uses one database with two sets of classes. A fuller form uses two databases, for example a relational one for writes and a document store for reads, kept in step by events. Event sourcing is one way to feed the read side, because the stored events can be replayed into any number of read models. It is not a requirement.

Why use it

Reads and writes often need different things. The write side needs rules and consistency. The read side needs speed and joins done in advance. With separate models, each can be shaped and scaled for its own job, and a read model can be rebuilt or added without touching the write side. Microsoft lists independent scaling, optimised schemas and simpler queries among the benefits.

The catch

The separate models are more code, and with separate stores the read side lags behind the write side. A customer places an order and the order list may not show it for a moment (see eventual consistencyEventual consistencyAfter a change, other services catch up later, and the design has to allow for the gap.Opens in a new tab). The design has to say what the screen does in that moment, for instance by showing the order from the command’s own result. Fowler is direct about the risk: he advises being very cautious, and says most of the cases he has met were not good ones. It fits a bounded part of a system with a complex domain or very different read and write loads. A simple CRUD screen does not need it.


Sources