- Programming Language: Go 1.26.0
- Container: Docker Compose
- Database: PostgreSQL 18 (separate write store + read store)
CQRS (Command Query Responsibility Segregation) splits a system into two models that are optimized independently:
- Commands (writes) hit a normalized write store — one fact in one place, enforced with foreign keys and transactions.
- Queries (reads) hit a denormalized read store — pre-joined, pre-aggregated rows answered with a single-row lookup.
The two stores are kept in sync asynchronously by a projector that drains a transactional outbox on the write side. That makes reads eventually consistent: a just-committed write is invisible to queries until the projector runs — and this project shows exactly that window.
Related projects:
simulate_read_write_splittingsplits reads/writes across copies of the same model; CQRS splits them across different models.simulate_query_cachingis a simpler form of the same read-optimization idea.
- Command/Query separation — different code paths, different schemas.
- Transactional outbox — events written atomically with the data.
- Projection — rebuild denormalized read rows from normalized events.
- Eventual consistency — query before vs after projection.
COMMAND ──► ┌─────────────── write_db (5460) ───────────────┐
PlaceOrder │ customers / orders / order_items (normalized) │
UpdateStatus │ events (outbox, written in same TX) │
└───────────────────────┬───────────────────────┘
│ poll unprocessed events
┌─────▼──────┐
│ Projector │ JOIN + aggregate
└─────┬──────┘
┌───────────────── read_db (5461) ──────────────┐
QUERY ─────►│ order_summary (denormalized, 1 row per order) │
GetOrderSummary└──────────────────────────────────────────────┘
docker compose up -d # write store + read store
docker compose ps # wait until both healthy
go mod tidy
go run main.goA placed order is immediately queryable on the write store but absent from the read model until the projector runs. After projection, the query returns a single denormalized row (customer name, item count, total). A status change repeats the cycle: stale read → project → fresh read.
| Aspect | Benefit | Cost |
|---|---|---|
| Read speed | Single-row lookups, no JOINs | Read model is derived/duplicated data |
| Write clarity | Normalized, transactional | Two schemas to maintain |
| Scaling | Scale reads and writes independently | Projector is extra moving infra |
| Consistency | Tunable, async | Reads are eventually consistent |
docker compose down -v