Lesson 5 · CQRS — commands, queries & the read/write split
CQRS & Fowler's caveat
Separate the model you write with from the model you read with — and know exactly how little of that this repo actually does, on purpose.
Your win: define CQRS honestly, quote Fowler's warning, and describe what this repo's version is — folders and handler types — and, just as importantly, what it is not.
The idea, in one sentence
Martin Fowler: CQRS is "the notion that you can use a different model to update information than the model you use to read information."1 A command changes state (create, update, delete). A query reads state and has no side effects. CQRS says: stop forcing both through one model — give writes their own path and reads their own path.
Why split? In a complex domain, the shape that's convenient for validating and saving a change is rarely the shape that's convenient for displaying data — one model doing both ends up doing neither well.
Fowler's caveat — say it in the interview
What this repo's CQRS actually is
Here's the honest version. In your services, CQRS is just two folders and two handler types:2
application/
├── commands/ // WRITE handlers: CreateEmailHandler, BulkCreateEmailsHandler …
├── queries/ // READ handlers: GetEmailEventHandler …
└── consumers/ // Kafka handlers (shared logic)
A command handler lives in commands/; a query handler in queries/. One
operation per handler, one handler per file. notification's system_notification module
is the same, and its own docs call it the "✅ canonical — copy this" module.3
That's the whole mechanism.
What it is NOT (so you don't invent it)
| Textbook CQRS sometimes has… | This repo |
|---|---|
| a command bus / mediator that routes commands to handlers | None. Controllers call handlers directly (Lesson 8). |
| event sourcing (rebuild state from an event log) | None. Plain rows in postgres. |
| separate read & write databases | One DB. The "split" is at the handler layer only. |
CQRS — Martin Fowler
Short, and the source of both the definition and the caveat. Read the whole thing; it's two screens and it will make you a more honest engineer about this pattern.
→ martinfowler.com/bliki/CQRS.html ·
threedots.tech — basic CQRS in Go
→ in-repo internal/spike/modules/email/application/{commands,queries}/
Check yourself (from memory)
Q1. What does CQRS separate?
Q2. How does this repo route a command to its handler?
Q3. What did Fowler warn about CQRS?
application/commands/ & application/queries/,
one op per handler/file. notification system_notification = the canonical module.
What it is NOT: no command bus/mediator (controllers call handlers directly), no
event sourcing, no separate read DB. The lightweight end of the spectrum, on purpose.1. Martin Fowler, CQRS (bliki, 2011).
2. In-repo: internal/spike/modules/email/application/{commands,queries}/; internal/spike/CLAUDE.md.
3. In-repo: internal/notification/CLAUDE.md (module table — system_notification "✅ Canonical — copy this").