When should I apply CQRS, and how do I sync the read and write models?
Question
In our app the write operations are very few (~5,000 records/day) but reads and complex reporting are very heavy (~2,000 queries/sec). The relational DB locks up because of the reporting queries. We want to fully separate the write model (PostgreSQL) and the read model (Elasticsearch or an optimized read-replica SQL) — CQRS. How do I keep the two models in sync, event-driven, with no lag and reliably?
Answer
Short answer: 2k reads/sec against 5k writes/day — that asymmetry is a real CQRS case. But start with the cheapest version; don’t jump straight to full CQRS.
The problem is clear: heavy reporting queries lock the write path too. Most of the time you don’t need a full architectural split to fix that.
- Add a read replica first. Put one or more read replicas on PostgreSQL and route all reporting queries there. Writes stay on the primary, reads go to the replica; the locking usually ends with just this. Don’t take on CQRS complexity before trying it.
- Understand when full CQRS earns its keep. A separate read model (Elasticsearch or denormalized SQL) is only worth it when query shapes diverge so much that one schema can’t serve both well. Needs like full-text search or heavy aggregation — where even a replica isn’t enough — are what justify splitting out the read model.
- Sync the models event-driven. Have the write side emit an event on every change; a projector consumes those events to update the read model. For reliability, use the outbox pattern: write the event to an outbox table in the same transaction as the business data, and have a separate process publish it — so you never get “the DB committed but the event was lost.”
- Accept eventual consistency, don’t dual-write. The read model lags the write by ms–seconds; design the UI for it (e.g. “saved” instantly, visible in the list a moment later). Never write to both stores from the app (dual-write) — they inevitably drift. Always drive the read model from the write side’s events.
Bottom line: replicas first, move reporting there. Adopt full CQRS only when query needs genuinely diverge, with outbox-driven projections — and treat the added complexity as a cost. The answer to “is PostgreSQL enough for me?” is still yes under most loads; push it to its limit before reaching for CQRS.
Related Reading
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.