The problem
One model has to serve both sides. Writes need validation and invariants, on a normalised shape. Reads need denormalised, pre-joined and filterable shapes, and there are often 100× more of them. Optimising one hurts the other.
How it works
- Commands express intent, may be rejected, and return little or nothing.
- Queries never change state.
- Projections build one or more read models, each shaped for a screen or API.
Levels of CQRS
- Code-level: separate command and query classes over the same DB. Cheap and often enough.
- Separate read tables or views in the same DB, updated in the same transaction.
- Separate read stores (Elasticsearch, Redis, a denormalised Mongo collection) updated asynchronously. Eventual consistency starts here.
- Read and write loads are very different
- Read screens need heavy joins or aggregates
- Combined with event sourcing or event-driven integration
- Simple CRUD — this doubles the model for no gain
- Users must always read their own write instantly and you can't design around it
Trade-offs
- ✅ Each side scales and is optimised independently.
- ✅ New read models can be added later by replaying events.
- ❌ Eventual consistency in the UI. Mitigate with "read your writes" tricks (return the new state from the command, or poll).
- ❌ More moving parts: projectors, rebuilds, monitoring projection lag.
Sources & further learning
Videos, courses, docs and books I recommend for this topic.
Related topics
Event Sourcing
Store every state change as an immutable event; current state is derived by replaying events. The log is the source of truth.
Event-Driven Architecture
Services announce facts ("EntryPublished") and others react asynchronously — decoupling producers from consumers in time, space and knowledge.
Caching Strategies
Cache-aside, read/write-through, write-behind; TTLs, invalidation, stampede protection and multi-layer caches (browser → CDN → app → DB).