# CQRS and Event Sourcing with NestJS: When Complexity is Worth It

> A practical guide to CQRS and event sourcing in NestJS — what the patterns actually solve, how to implement them with @nestjs/cqrs, and Martin Fowler's honest warning about when to skip them.

URL: https://www.matthewswong.com/en/blog/cqrs-event-sourcing-nestjs/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2025-06-10
- Updated: 2026-10-17
- Category: DevOps

Martin Fowler's warning about CQRS is one of the most-cited cautions in software architecture: 'For most systems CQRS adds risky complexity, and many information systems fit well with the notion of an information base that is updated in the same way that it's read.' He's right. I've built ERP modules where a simple service layer with PostgreSQL read and writes was the correct architecture. CQRS earned its complexity on a specific module — the approval workflow engine — where the read model (who can approve next, what's pending) was genuinely different from the write model (recording approval events). This guide covers what CQRS and event sourcing actually solve, and when they're worth the overhead.

## CQRS: Separating Commands from Queries

CQRS (Command Query Responsibility Segregation) was first described by Greg Young, building on Bertrand Meyer's Command-Query Separation principle. The core idea: use a different model to update information than the model you use to read it. In practice, this means your application has two stacks — Commands (write operations that change state) and Queries (read operations that return data). Commands go through a command bus and are handled by command handlers that update the write model. Queries go through a query bus to query handlers that read from a potentially separate read model (could be the same database, or a denormalized projection table, or Redis cache).

### What CQRS Actually Enables

The real power of CQRS emerges when your read model and write model need to be different. An approval workflow has a complex write model (business rules about who can approve what, state machine transitions, audit trail) and a simple read model (show me pending approvals for this user). Storing these as the same data model means either compromising the write model to make reads efficient or vice versa. Separating them lets you optimize each independently. You can also scale them independently — read traffic might be 100x write traffic, so a separate read model served from Redis makes sense.

> A tip from designing approval workflows in ERP-style systems: use CQRS for modules where the read model is materially different from the write model — where you'd otherwise have complex joins or denormalized fields just to serve read queries. Skip CQRS for CRUD-heavy modules like user profiles, address books, or simple settings pages. The @nestjs/cqrs package makes the pattern accessible, but applying it indiscriminately adds boilerplate without benefit.

## Event Sourcing: Storing Events, Not State

Event sourcing stores the sequence of events that led to the current state, rather than the current state itself. Instead of UPDATE invoices SET status='paid', you append an InvoicePaid event to an event log. The current state is derived by replaying the event history. The benefits: complete audit trail (every state change is recorded with who did it and when), event replay (rebuild any read model from the event log), and temporal queries (what was the state of this invoice on any past date?). The costs: event schema evolution is harder than relational schema migration, and reads require event replay or maintaining up-to-date projections.

### Implementing CQRS with @nestjs/cqrs

NestJS provides a @nestjs/cqrs package that implements the Command Bus, Query Bus, and Event Bus patterns. Commands are plain TypeScript classes with properties. CommandHandlers implement the ICommandHandler interface and are registered in the module. Events are emitted after state changes and handled by EventHandlers for side effects (sending emails, updating projections, triggering other commands). The implementation is clean and testable — each handler is a small, focused class with a single responsibility.

> **Event sourcing makes simple things hard**

> Think carefully before committing to event sourcing for an entire application. Schema evolution of events is painful — if you add a required field to an event, you need migration logic to backfill it when replaying old events. Querying event-sourced data requires either maintaining projection tables (adding operational complexity) or replaying events on every read (adding latency). Debugging is harder — you need to replay event history to understand current state. Greg Young himself said in 2016 that CQRS 'was a product of its time' and recommends using it only for specific bounded contexts, not system-wide.

## Event Sourcing in Practice: Projections

Projections are the read-side of event sourcing — they subscribe to events and maintain a denormalized, query-optimized view of the data. For an approval workflow: the ApprovalProjection listens to ApprovalRequested, ApprovalGranted, and ApprovalRejected events and maintains a table showing each request's current status and next approver. The projection can be rebuilt from scratch by replaying all events — this is the 'time travel' capability that makes event sourcing valuable for audit-heavy systems. Use PostgreSQL JSONB columns for your event store — cheap, queryable, and no additional infrastructure.

## Sagas: Coordinating Long-Running Processes

In @nestjs/cqrs, a Saga is a class that listens to domain events and dispatches commands in response. Sagas handle the choreography of multi-step business processes — when InvoiceCreated fires, the saga dispatches GeneratePdfCommand and SendEmailCommand. Sagas are the event-driven glue between bounded contexts. They must be idempotent — if the same event is processed twice (due to retry), the saga should not dispatch duplicate commands. Use correlation IDs on commands and events to detect and skip duplicates.

## My Honest Take: CQRS Without Event Sourcing

The most pragmatic version of this pattern is CQRS without event sourcing. Separate your command handlers (write operations that use the full domain model) from your query handlers (reads that query optimized projections or views) — but still store state as current state in PostgreSQL, not as an event log. This gives you most of the architectural benefits (separation of concerns, independent scaling of read and write paths, clean code organization) without the operational complexity of event sourcing. Apply event sourcing selectively — only for bounded contexts where complete audit history, event replay, or temporal queries are genuine product requirements, not just 'nice to have.'

## Sources & Further Reading

- Martin Fowler — CQRS (bliki)
- Greg Young — CQRS and Event Sourcing (2010)
- NestJS Docs — CQRS Module

## Source links

- https://martinfowler.com/bliki/CQRS.html
- https://docs.nestjs.com/recipes/cqrs

## FAQ

### When is CQRS actually worth the added complexity?

CQRS earns its complexity when the read model and write model are genuinely different — for example, an approval workflow where the write model handles state machine transitions and audit rules while the read model just shows pending approvals per user. For CRUD-heavy modules like user profiles or address books, a simple service layer with PostgreSQL is the correct architecture. Martin Fowler himself warns that for most systems CQRS adds risky complexity.

### What is the difference between CQRS and Event Sourcing — do they have to be used together?

CQRS and Event Sourcing are separate patterns that are often discussed together but can be used independently. CQRS separates the write model (commands) from the read model (queries), while Event Sourcing stores the sequence of events that led to current state instead of current state itself. The post recommends CQRS without Event Sourcing as the most pragmatic approach — you get the architectural benefits of separated read/write paths without the operational overhead of managing an event log.

### How does @nestjs/cqrs implement the CQRS pattern?

The @nestjs/cqrs package provides a Command Bus, Query Bus, and Event Bus. Commands are plain TypeScript classes; CommandHandlers implement the ICommandHandler interface and are registered in the module. Queries follow the same pattern via QueryHandlers. Events are emitted after state changes and processed by EventHandlers for side effects like sending emails or updating projections.

### What are Sagas in @nestjs/cqrs and why must they be idempotent?

A Saga in @nestjs/cqrs listens to domain events and dispatches commands in response, acting as the event-driven glue between bounded contexts for multi-step business processes — for instance, dispatching GeneratePdfCommand and SendEmailCommand when an InvoiceCreated event fires. Sagas must be idempotent because the same event can be processed more than once due to retries; the post recommends using correlation IDs on commands and events to detect and skip duplicates.

### What are the main drawbacks of Event Sourcing to consider before adopting it?

Event schema evolution is harder than relational schema migration — adding a required field to an event requires migration logic to backfill old replays. Reads require either maintained projection tables (adding operational complexity) or replaying events on every read (adding latency). Debugging is also harder because you must replay event history to understand current state. Greg Young, who popularized the pattern, recommends using it only for specific bounded contexts where complete audit history or temporal queries are genuine product requirements.

---

- HTML version: https://www.matthewswong.com/en/blog/cqrs-event-sourcing-nestjs/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/cqrs-event-sourcing-nestjs/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
