Skip to content
Muhammet Şafak
tr
Asked by: Rüzgar Answered:

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.

Short answer

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. If you really are moving the read model onto Elasticsearch, the search side brings costs of its own; I worked through those in the edge n-gram answer.

Why

  1. Full CQRS only earns its keep when query shapes diverge. 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.
  2. Dual-writes inevitably drift. If the app writes to both stores, the two sides diverge over time and you can no longer say which one is right.
  3. The read model always lags. It trails the write by ms–seconds; eventual consistency isn’t a defect, it’s the price of this architecture.

What to do

  1. 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.
  2. 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.
  3. Use the outbox pattern for reliability. 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.”
  4. Design the UI for eventual consistency. For example, “saved” instantly, visible in the list a moment later. Always drive the read model from the write side’s events, and never write to both stores from the app.

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

Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

More Questions

All questions

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind