When CQRS and Event Sourcing Pay Off
The conference talk showed event sourcing like default architecture—every button click an immutable fact, read models materializing like magic. Six months later the team debugged why inventory projection lagged checkout by four minutes during peak. CQRS (Command Query Responsibility Segregation) and event sourcing solve specific pains: asymmetric read/write load, audit trails, and temporal reconstruction. They also introduce dual models, eventual consistency, and replay machinery. Use them when the business value exceeds the tax—not because DDD blogs said so.
CQRS without event sourcing
Command → Write model (normalized DB)
↓ publish domain event
Read model (denormalized Elasticsearch)
Query ← Read model only
Writes optimize for invariants; reads optimize for UI screens. Accept eventual consistency between models—display "processing" states honestly.
Validate this in staging with production-like data volume before declaring done. Capture metrics baseline the week before change and compare for seven days after—subtle regressions hide in aggregates until a large tenant hits the path. Update the on-call runbook with the failure signature and rollback command so responders need not rediscover steps during an incident.
Event sourcing core loop
def handle(cmd: PlaceOrder):
order = Order.create(cmd)
events = order.pull_uncommitted_events()
event_store.append(stream_id=order.id, events=events)
publish(events)
State equals fold(events). Snapshots optional for long streams.
Validate this in staging with production-like data volume before declaring done. Capture metrics baseline the week before change and compare for seven days after—subtle regressions hide in aggregates until a large tenant hits the path. Update the on-call runbook with the failure signature and rollback command so responders need not rediscover steps during an incident.
When the tradeoff favors ES/CQRS
| Signal | Fit |
|---|---|
| Regulatory audit of every change | Strong |
| Multiple read views same writes | Strong |
| Complex domain with rich behavior | Moderate |
| Simple CRUD admin | Weak |
| Strong immediate read-your-writes everywhere | Weak |
Validate this in staging with production-like data volume before declaring done. Capture metrics baseline the week before change and compare for seven days after—subtle regressions hide in aggregates until a large tenant hits the path. Update the on-call runbook with the failure signature and rollback command so responders need not rediscover steps during an incident.
Projection rebuild
Bug in projector? Fix code, reset checkpoint, replay from offset—or rebuild read DB from scratch. Automate replay in staging on every release. Document RPO for projection lag.
Validate this in staging with production-like data volume before declaring done. Capture metrics baseline the week before change and compare for seven days after—subtle regressions hide in aggregates until a large tenant hits the path. Update the on-call runbook with the failure signature and rollback command so responders need not rediscover steps during an incident.
Command handling idempotency
Commands carry command_id; dedupe at aggregate level. Retries are normal with at-least-once messaging.
Validate this in staging with production-like data volume before declaring done. Capture metrics baseline the week before change and compare for seven days after—subtle regressions hide in aggregates until a large tenant hits the path. Update the on-call runbook with the failure signature and rollback command so responders need not rediscover steps during an incident.
Avoid distributed monolith
CQRS across fourteen microservices without bounded context discipline creates chatty command buses. Start modular monolith with in-process handlers; extract when scaling proves need.
Validate this in staging with production-like data volume before declaring done. Capture metrics baseline the week before change and compare for seven days after—subtle regressions hide in aggregates until a large tenant hits the path. Update the on-call runbook with the failure signature and rollback command so responders need not rediscover steps during an incident.
Event sourcing for Order aggregate only; Customer stays CRUD. Not every table earns a stream.
Projection rebuild automation in staging on every release. Bug in projector: fix, reset checkpoint, replay.
Hybrid path: event source Order only; Customer CRUD. Not every table earns a stream.
Unbounded microservices CQRS without bounded contexts creates chatty buses—start modular monolith handlers first.
Validate this in staging with production-like data volume before declaring done. Capture metrics baseline the week before change and compare for seven days after—subtle regressions hide in aggregates until a large tenant hits the path. Update the on-call runbook with the failure signature and rollback command so responders need not rediscover steps during an incident.
Document the decision, owner, and rollback path in your team wiki the same week you ship. Future you will not remember which environment variable toggled the behavior unless it is written next to the runbook entry and linked from the alert. That habit costs ten minutes per change and saves hours when pagination or auth misbehaves under a single large tenant.
Run the change through your standard PR checklist: tests, observability, and a two-minute rollback drill in staging. Small operational habits accumulate into systems that survive on-call nights without heroics.
Share a short write-up in your engineering channel after rollout: what shipped, what metric you watch, and who owns follow-up. That closes the loop for teammates who were not in the PR and surfaces gaps in docs before the next person repeats the same investigation.
Prefer boring, repeatable process over one heroic migration weekend.
Treat operational readiness as part of definition-of-done: dashboards, alerts, runbook links, and a named owner. Skipping those steps ships code that works in demo and fails quietly in production until a customer or auditor finds the gap.
Resources
- CQRS (Martin Fowler)
- Event Sourcing (Martin Fowler)
- Greg Young: CQRS Documents
- EventStoreDB documentation
- Microsoft Azure: CQRS pattern
Frequently asked questions
Do I need event sourcing if I use CQRS?
No. CQRS separates write models from read models; event sourcing persists state as a sequence of events instead of current row values. You can use CQRS with traditional CRUD writes projecting to read databases. Event sourcing adds auditability and temporal queries but increases storage, versioning, and operational complexity.
What problems does event sourcing actually solve?
Complete audit history, reconstructing state at any point in time, multiple read projections from one write stream, and debugging production by replaying events. It fits domains where history is legally significant—finance, healthcare—or where integrators need rich domain events. CRUD with updated_at solves most inventory dashboards fine.
What is the biggest operational downside?
Schema evolution on stored events—upcasting old event versions during replay—and rebuilding projections after bugs or new query requirements. Event store becomes critical infrastructure; backup and replay testing are mandatory. Team must think in events, not tables, across product and engineering.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →