Minimal CQRS: separate code paths, keep one application and one database
· 4 min readengineering 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.
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:
- A command and the data it changes can use one transaction.
- A query immediately after a successful command can read the new state.
- There is one deployment and one place to diagnose persistence problems.
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.