Command ≠ query. Two models when one relational model already lies to one of the sides. It is not two microservices nor mandatory Event Sourcing.
Domain: ddd. Command persistence: aggregate. Integration: event-driven-architecture. Decision: software-architecture.
When (not) to use
Use when: the read screen does not fit the aggregate; writes with heavy invariant; denormalized read store (search, dashboard).
Do not use when: symmetric CRUD. “Every MediatR handler” without two models is theater.
Command vs query
| Command | Query | |
|---|---|---|
| Intent | Change state | Answer question |
| Model | Aggregate / write store | DTO / view / read store |
| Idempotency | Explicit in critical writes | No effect |
| Failure | 4xx from invariant | empty 404 |
- Commands:
PlaceOrder. They do not return the screen graph — they accept202+ id or the minimum state. - Queries: do not go through the aggregate just to “reuse”.
GetOrderDetailsreads the projection or read SQL.
Client → PlaceOrder (write DB)
→ OrderPlaced → projector → read DB / cache
Client → GetOrderHistory (read)
One database vs two
- Same DB, write tables + read views/tables: lightweight CQRS. Local transaction possible.
- Two stores (Postgres + Elasticsearch): eventual consistency. UI handles delay. Outbox for projecting.
Do not start with two databases. Measure the query.
Synchronize the read model
- After the write commit: in-process projector, or event + consumer (
event-driven-architecture). - Projection idempotent (upsert by
order_id+ version). - Version/event on write prevents old projections from overwriting new ones.
Event sourcing (optional)
The write model is the event stream. Aggregate rebuild = fold.
Only if history is a requirement (legal audit, time-travel, truth = facts). Cost: upcasting, snapshots, mandatory projections, team learning curve.
Do not do ES “to have CQRS”. CQRS lives without ES.
Application
- Command bus (MediatR, etc.) is not CQRS. It is a dispatcher. CQRS = distinct models.
- Authz on the command (can mutate?) and on the query (can view?) —
authentication-authorization. - Clean: port of
OrderRepository(write) ≠OrderDetailsQuery(read) —clean-architecture.
Anti-patterns
- Command that returns the entire page “to avoid doing a GET”
- Query that calls three services and persists
- Event sourcing in user registration
- Two microservices “command-service” and “query-service” on day 1
- Non-idempotent projection
Integration with other skills
- Aggregates:
ddd - Events/outbox:
event-driven-architecture - Modules:
modular-monolith - Services:
microservices-architecture - HTTP:
api-design(POSTcommand,GETquery)
Completion criteria
- Commands and queries with justified models
- Read model updated idempotently if separate
- No ES unless history is the product
- Authz on both sides
- UI aware of eventual delay if there are two stores