Skip to main content
Home
HexagonalGuru

CQRS and Event-Driven: When They're Worth It and When They're Overengineering

HexagonalGuru's avatar

HexagonalGuru

CQRS and Event-Driven: When They're Worth It and When They're Overengineering

A five-person team maintains an invoicing CRUD. Six months after someone read an architecture book, the system has two databases, an event broker, nine projections and a diagram nobody can draw from memory anymore. Invoicing still does exactly the same as before — it just takes longer to change and fails in new and exciting ways.

CQRS and event-driven architecture are two of the most powerful tools in the modern repertoire. They're also among the most abused. This guide is about the second part: what they are without evangelism, which signals tell you that you genuinely need them, and how to adopt them in stages without mortgaging the team.

What They Are, in One Sentence Each

CQRS (Command Query Responsibility Segregation): the model that writes and the model that reads are different things. Operations that change state (commands) use a model optimised to protect business rules; queries use another one optimised to return data fast and in the exact shape each screen needs.

Event-driven architecture: components don't call each other directly; they announce facts — "InvoiceIssued", "OrderCancelled" — and whoever needs them reacts. The emitter doesn't know who's listening, and that's precisely what makes it powerful.

And one clarification that saves a lot of money: they're not a package deal. CQRS doesn't require events and events don't require CQRS. Buying them together "because they go hand in hand" is like buying the trailer before you own the car.

What Actually Changes (and What Doesn't)

CQRS architecture diagram: the command side validates business rules and writes to its database, events update the projections and the query side reads from optimised models

When the decision is the right one, this is what changes:

  • Reads stop fighting writes. The invoice listing with twelve filters and three totals no longer shares a model with the logic that validates an issued invoice can't be edited. Each side evolves at its own pace.
  • Writes focus on the rules. The command model carries no screen DTOs or queries: it exists to protect invariants. It's where DDD shines — if your domain justifies it, as we explain in the practical Domain-Driven Design guide.
  • Events decouple in time. If the service sending the confirmation email is down, the order still gets confirmed; the event waits. Failures stop cascading.
  • Each consumer grows without bothering anyone. Tomorrow a new report appears, an accounting integration or an audit: they subscribe to events that already exist without touching the code that emits them.

And what does not change — or changes for the worse if you ignore it: consistency goes from immediate to eventual ("the order is there, the listing will see it in a second"), debugging means following messages across systems, infrastructure grows (broker, projections, consumers) and the team needs craft. CQRS and events don't remove complexity: they move it somewhere it only deserves to be if it pays you back.

The Signs That Say Yes

CQRS pays its bills when several of these conditions hold at once:

  • Complex write rules. Changing state means validating serious invariants, not "save the form". If your write logic fits in a controller, you don't need a command model.
  • Radically different read shapes. The same concept is queried as a paginated listing, an aggregated report, a public API and an Excel export — and every query tortures the write model a bit more.
  • Brutal read/write asymmetry. Thousands of reads per write justify optimising each side separately, even with different stores.
  • Several consumers of the same fact. When "OrderConfirmed" must trigger email, warehouse, invoicing and analytics, the event is the natural shape; chaining synchronous calls is a coupling bomb.
  • Traceability as a requirement. If business or regulation demands knowing what happened and when, events give you the history almost for free (and event sourcing, which is a further step that's almost never needed, gives you the full history).

The Signs That Say No

Overengineering has its symptoms too, and they're just as recognisable:

  • A CRUD in disguise. Create, read, update, delete — no business rules to protect. Putting CQRS here is buying a truck to carry your backpack to work.
  • Small team, new pattern. If nobody has operated events in production, the team's first event-driven system shouldn't be a critical one. The learning curve is paid in incidents.
  • Immediate consistency is mandatory. If the user must see their change reflected that very instant and there's no room for "eventual", async projections are a problem, not a solution.
  • "In case we scale". Real scaling is a wonderful problem that gets solved when it arrives, with metrics in hand. Designing for the traffic you imagine instead of the traffic you have is the most expensive way to guess the future.

The Incremental Path (Stop Where It Stops Paying Off)

The biggest trap isn't technical: it's adopting the full pattern at once. The healthy path has four steps, and each one adds value even if you never climb the next:

Incremental CQRS and events adoption ladder: separate read and write code, then separate models on the same database, then projections with their own storage, and finally an event broker with multiple consumers
  1. Separate read code from write code. Same model, same database, but command handlers don't return screen data and queries don't touch the domain model. It costs a day of discipline and already clarifies the architecture.
  2. CQRS-lite: two models, one database. Queries use plain SQL or views with purpose-built DTOs; commands use the rich model. No broker, no projections, no new infrastructure. This is where most applications that "needed CQRS" live happily.
  3. Projections with their own storage. When reads demand shapes or volume the transactional database can't provide, projections are fed from internal events. Eventual consistency appears: introduce it only where the business accepts it.
  4. Events with a broker and multiple consumers. When several systems react to the same facts, in comes the broker (RabbitMQ, Kafka, whatever fits your operations). Not before: a broker without multiple consumers is a queue with delusions of grandeur.

It's no coincidence that this path is cheap to walk: it's exactly what hexagonal architecture makes easy. If ports and adapters isolate the domain from infrastructure, swapping "internal event" for "event on RabbitMQ" means changing an adapter, not rewriting the system.

Common Mistakes

  1. CQRS across the whole system. The pattern applies per bounded context, not per application. Invoicing may need it and the country catalogue doesn't. A healthy system mixes: CQRS where it hurts, CRUD where it's plenty.
  2. Event sourcing thrown in for free. Storing the full event history as the source of truth is a different, far more demanding pattern that CQRS doesn't require. If someone proposes it, ask them to explain which regulation or which time-travel analysis justifies it.
  3. Events that are RPC in disguise. If the emitter waits for a response from the "event", that's a remote call with extra latency. Events announce past facts; commands request future actions. Confusing the two breaks the entire mental model.
  4. Eventual consistency without telling the business. The user who pays and doesn't see the payment on their dashboard for two seconds will call support. The UI must be designed for eventualities: "processing" states, email confirmations, refreshes. That's a product decision, not just a technical one.
  5. An unmonitored broker. A queue that grows without alerts is an incident maturing in silence. Consumer lag, dead letters and retries are first-class metrics from day one.

In Short

CQRS separates writing from reading; events turn calls into facts. They're worth it when there are complex write rules, demanding reads, load asymmetry or several consumers of the same fact — and they're overengineering in a CRUD, in a team without the craft, or when immediate consistency is sacred. The healthy path is incremental: separate code, separate models, project, and only then a broker. Every step adds value; the mistake is trying to climb the ladder in one leap.

If you suspect your system needs this leap — or already took it and now nobody understands anything — an architecture audit makes it clear in days, with a staged plan. Let's talk about your case →

Keep reading: What is Domain-Driven Design: a practical guide with a real example.

  • #ddd
  • #cqrs
  • #event-driven

Related articles

Shall we start?

Ready to build something that grows with your business?

Tell us your goals and together we will map the design, development and technology route that takes you from vision to measurable results.