Minimal CQRS: separate code paths, keep one application and one database

· 4 min read

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

The first post showed why a changed DTO can hide a business action.1 The second made that action explicit with commands.2

The next step does not need a new service, a queue, or a second database. Keep one deployed application and one database. Split the code paths according to the job they do.

One application, two responsibilities

A command changes the system. It loads the state needed to apply a rule, checks whether the request is valid, and saves the result.

A query answers a question. It returns data shaped for its caller and should not run the command-side rules just to build a screen.

Mermaid diagram

Both paths use the same database and deploy together. They differ in code because they have different work to do. Microsoft's CQRS guidance describes the pattern as separating the models for reading and writing, while noting that a shared data store can still be appropriate.[2]

The command path protects the change

Suppose an order is placed. The request is a command, not an edited order object:

public sealed record PlaceOrder(Guid CustomerId, IReadOnlyList<OrderLine> Lines);

Its handler loads the customer and the items it needs, checks stock and ordering rules, creates the order, and commits the transaction. The command may fail because the request is not allowed. That is part of the API contract.

The write model does not need to contain every field a dashboard might display. It needs enough behavior and state to decide whether PlaceOrder is valid.

The query path shapes the answer

The order list asks a different question:

public sealed record OrderSummaryDto(
    Guid Id,
    string Number,
    string CustomerName,
    decimal Total,
    string Status);

GetOrderSummary can project directly from the same database into that DTO. It does not need to rebuild an Order aggregate, call PlaceOrder rules, or return fields that the list page will discard.

The database is shared, but the models are not forced to be identical. Greg Young describes CQRS as a separation of read and write responsibilities, with different models only where the responsibility needs them.[1]

CQS and CQRS are different boundaries

CQS Minimal CQRS
Main boundary An operation either changes state or returns information. Command and query code follow separate paths.
Scope A method or API operation. The application structure around those operations.
Database Unchanged. Still one database.
Example PlaceOrder and GetOrderSummary have different contracts. Separate handlers and models process those requests.

CQS makes each operation easier to read. CQRS makes the split visible in the application. A system can use CQS without reorganizing its code, and it can introduce this minimal CQRS boundary without any distributed infrastructure.

What this does not buy

One application and one database keep useful properties:

It does not create independent read scaling, denormalized reporting projections, or isolation from a busy write workload. Those are separate costs and decisions. The next post covers what changes when a team separates the models or stores further.

Start with the boundary you need

Use a separate command and query path when the write side has meaningful rules or the read side needs a shape that does not belong in the write model. Keep the application and database together until a demonstrated problem requires more separation.

Sources

  1. Greg Young, CQRS Documents
  2. Microsoft Learn, "CQRS pattern"

Footnotes

  1. When CRUD stops describing the work
  2. CQS and commands: make the application understand intent
Share on LinkedIn