Skip to content

// Architecture Styles · 06

CQRS

Commandleads towrite DB · read DBQuery

Split writing from reading so each side can scale on its own.

How it fits together

CQRS structure«client»ClientCommand handlerPlaceOrderloads aggregate via ORMenforces invariantsQuery handlerGetOrderSummariesuntracked / plain SQLreturns DTOsWrite DBnormalized tablesRead modeldenormalized viewcommandwritesqueryreadsprojection
uses / callsreplicates / copies
  • Client uses / calls Command handler (command)
  • Command handler uses / calls Write DB (writes)
  • Client uses / calls Query handler (query)
  • Query handler uses / calls Read model (reads)
  • Write DB replicates / copies Read model (projection)

Spot it

  • Queries contort the domain model
  • Read screens need wide joins and projections
  • Reads and writes scale very differently

It costs

Two models to maintain, and with separate stores reads lag behind writes.

In code

public sealed record PlaceOrder(Guid CustomerId, IReadOnlyList<OrderLine> Lines);​public sealed class PlaceOrderHandler(ShopDbContext db){    public async Task<Guid> HandleAsync(PlaceOrder command, CancellationToken ct)    {        var customer = await db.Customers.SingleAsync(c => c.Id == command.CustomerId, ct);        var order = customer.PlaceOrder(command.Lines);        db.Orders.Add(order);        await db.SaveChangesAsync(ct);        return order.Id;    }}​public sealed record OrderSummary(Guid Id, DateTimeOffset PlacedAt, decimal Total, string Status);​public sealed class GetOrderSummariesHandler(ReadDbContext db){    public Task<List<OrderSummary>> HandleAsync(Guid customerId, CancellationToken ct) =>        db.OrderSummaries            .AsNoTracking()            .Where(o => o.CustomerId == customerId)            .OrderByDescending(o => o.PlacedAt)            .Select(o => new OrderSummary(o.Id, o.PlacedAt, o.Total, o.Status))            .ToListAsync(ct);}
Go deeperThe full explanation and what to ask for · 1 min readExpand

Command Query Responsibility Segregation: writes go through a model built to enforce invariants, reads through one built to answer questions fast. It grew out of command-query separation at the method level.

It comes in degrees. Separate handlers over the same database are cheap and often worth it. Separate read stores fed by projections add eventual consistency and rebuild tooling, so reach for them only when read load or shape demands it. Simple CRUD gains nothing.

It pairs naturally with event sourcing, because an event log is awkward to query directly, but neither requires the other.

Ask for it

  • command
  • query
  • write model
  • read model
  • projection
  • eventual consistency

Split this feature into commands and queries. Commands load aggregates through the ORM and enforce the rules; queries skip the domain and project straight to DTOs with untracked reads or plain SQL. Keep one database for now.