As applications grow in complexity, traditional CRUD architectures often become difficult to maintain, scale, and audit. Business requirements such as tracking historical changes, rebuilding system state, and handling high-volume workloads can expose the limitations of a single data model.
Two architectural patterns that address these challenges are CQRS (Command Query Responsibility Segregation) and Event Sourcing.
CQRS separates write operations from read operations, allowing each side to evolve independently. Event Sourcing takes this further by storing every change as an immutable event, creating a complete audit trail of the application's history.
In this tutorial, you'll learn how to implement CQRS and Event Sourcing in NestJS using the official CQRS module. We'll build a simple Order Management System where commands create orders, events record changes, and projections build queryable read models.
By the end of this guide, you'll understand:
- CQRS fundamentals
- Event Sourcing concepts
- NestJS CQRS module
- Commands, Queries, Events
- Event Handlers and Sagas
- Read Model projections
- Benefits and trade-offs of Event-Driven Architecture
Prerequisites
Before starting, ensure you have:
- Node.js 22 or later
- NestJS CLI installed
- TypeScript knowledge
- Basic understanding of NestJS modules and services
Install NestJS CLI if needed:
npm install -g @nestjs/cli
Create a new project:
nest new nestjs-cqrs-demo
Move into the project:
cd nestjs-cqrs-demo
Understanding CQRS
CQRS stands for Command Query Responsibility Segregation.
Instead of one model handling everything:
Application
├─ Create
├─ Update
├─ Delete
└─ Read
CQRS separates responsibilities:
Commands (Write Side)
├─ Create Order
├─ Update Order
└─ Cancel Order
Queries (Read Side)
├─ Get Order
├─ List Orders
└─ Reporting
Benefits
- Better scalability
- Independent optimization of reads and writes
- Easier maintenance
- Supports event-driven architectures
- Improved auditing
Understanding Event Sourcing
Traditional applications store only the current state:
{
"id": 1,
"status": "SHIPPED"
}
Event Sourcing stores every state change:
[
{
"event": "OrderCreated",
"status": "PENDING"
},
{
"event": "OrderConfirmed",
"status": "CONFIRMED"
},
{
"event": "OrderShipped",
"status": "SHIPPED"
}
]
The current state is reconstructed by replaying events.
Advantages
- Complete audit history
- Easy debugging
- Temporal queries
- Event replay capabilities
- Integration with messaging systems
Installing CQRS Support
NestJS provides official CQRS support.
Install the package:
npm install @nestjs/cqrs
Project Structure
We'll organize the application as follows:
src/
├── orders/
│ ├── commands/
│ ├── events/
│ ├── queries/
│ ├── handlers/
│ ├── projections/
│ ├── order.aggregate.ts
│ └── orders.module.ts
├── app.module.ts
Creating the Order Aggregate
In Event Sourcing, aggregates are responsible for business logic and generating events.
src/orders/order.aggregate.ts
import { AggregateRoot } from '@nestjs/cqrs';
import { OrderCreatedEvent } from './events/order-created.event';
export class OrderAggregate extends AggregateRoot {
constructor(
public readonly id: string,
public status: string = 'PENDING',
) {
super();
}
create() {
this.apply(new OrderCreatedEvent(this.id));
}
}
Defining Events
Events represent facts that have already happened.
src/orders/events/order-created.event.ts
export class OrderCreatedEvent {
constructor(
public readonly orderId: string,
) {}
}
Later, you could add:
OrderConfirmedEvent
OrderShippedEvent
OrderCancelledEvent
Creating Commands
Commands express user intentions.
src/orders/commands/create-order.command.ts
export class CreateOrderCommand {
constructor(
public readonly orderId: string,
) {}
}
Creating Command Handlers
Command handlers execute business operations.
src/orders/handlers/create-order.handler.ts
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { CreateOrderCommand } from '../commands/create-order.command';
import { OrderAggregate } from '../order.aggregate';
@CommandHandler(CreateOrderCommand)
export class CreateOrderHandler
implements ICommandHandler<CreateOrderCommand>
{
async execute(command: CreateOrderCommand) {
const order = new OrderAggregate(command.orderId);
order.create();
order.commit();
return {
id: command.orderId,
status: 'PENDING',
};
}
}
The handler:
- Creates aggregate
- Applies event
- Commits event
- Returns result
Creating Event Handlers
Event handlers react to events.
src/orders/handlers/order-created.handler.ts
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { OrderCreatedEvent } from '../events/order-created.event';
@EventsHandler(OrderCreatedEvent)
export class OrderCreatedHandler
implements IEventHandler<OrderCreatedEvent>
{
handle(event: OrderCreatedEvent) {
console.log(
`Order created: ${event.orderId}`,
);
}
}
This handler could:
- Send emails
- Publish Kafka messages
- Update projections
- Trigger workflows
Building a Read Model Projection
Read models optimize query performance.
src/orders/projections/orders.projection.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class OrdersProjection {
private readonly orders = new Map();
add(order: any) {
this.orders.set(order.id, order);
}
findById(id: string) {
return this.orders.get(id);
}
findAll() {
return [...this.orders.values()];
}
}
Updating Projection Through Events
Modify the event handler:
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { OrderCreatedEvent } from '../events/order-created.event';
import { OrdersProjection } from '../projections/orders.projection';
@EventsHandler(OrderCreatedEvent)
export class OrderCreatedHandler
implements IEventHandler<OrderCreatedEvent>
{
constructor(
private readonly projection: OrdersProjection,
) {}
handle(event: OrderCreatedEvent) {
this.projection.add({
id: event.orderId,
status: 'PENDING',
});
}
}
Now the read side stays synchronized with the event stream.
Creating Queries
Queries retrieve data.
src/orders/queries/get-order.query.ts
export class GetOrderQuery {
constructor(
public readonly id: string,
) {}
}
Creating Query Handlers
src/orders/handlers/get-order.handler.ts
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { GetOrderQuery } from '../queries/get-order.query';
import { OrdersProjection } from '../projections/orders.projection';
@QueryHandler(GetOrderQuery)
export class GetOrderHandler
implements IQueryHandler<GetOrderQuery>
{
constructor(
private readonly projection: OrdersProjection,
) {}
async execute(query: GetOrderQuery) {
return this.projection.findById(query.id);
}
}
Registering CQRS Components
src/orders/orders.module.ts
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { OrdersProjection } from './projections/orders.projection';
import { CreateOrderHandler } from './handlers/create-order.handler';
import { OrderCreatedHandler } from './handlers/order-created.handler';
import { GetOrderHandler } from './handlers/get-order.handler';
const CommandHandlers = [
CreateOrderHandler,
];
const EventHandlers = [
OrderCreatedHandler,
];
const QueryHandlers = [
GetOrderHandler,
];
@Module({
imports: [CqrsModule],
providers: [
OrdersProjection,
...CommandHandlers,
...EventHandlers,
...QueryHandlers,
],
})
export class OrdersModule {}
Creating a Controller
src/orders/orders.controller.ts
import {
Controller,
Post,
Get,
Param,
} from '@nestjs/common';
import {
CommandBus,
QueryBus,
} from '@nestjs/cqrs';
import { randomUUID } from 'crypto';
import { CreateOrderCommand } from './commands/create-order.command';
import { GetOrderQuery } from './queries/get-order.query';
@Controller('orders')
export class OrdersController {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {}
@Post()
create() {
const id = randomUUID();
return this.commandBus.execute(
new CreateOrderCommand(id),
);
}
@Get(':id')
getOrder(
@Param('id') id: string,
) {
return this.queryBus.execute(
new GetOrderQuery(id),
);
}
}
Running the Application
Start the server:
npm run start:dev
Create an order:
curl -X POST http://localhost:3000/orders
Response:
{
"id": "b9a6f86d-7a66-4b6c-98df-fbd6c93b42d",
"status": "PENDING"
}
Fetch the order:
curl http://localhost:3000/orders/{id}
Adding a Real Event Store
The in-memory implementation is suitable for learning but not production.
Popular event stores include:
| Solution | Description |
|---|---|
| EventStoreDB | Purpose-built event database |
| PostgreSQL | Event tables with append-only writes |
| MongoDB | Event documents collection |
| Kafka | Distributed event streaming |
| RabbitMQ | Event messaging integration |
A typical event table:
CREATE TABLE order_events (
id UUID PRIMARY KEY,
aggregate_id UUID,
event_type VARCHAR(100),
event_data JSONB,
created_at TIMESTAMP
);
Implementing Sagas
Sagas coordinate long-running workflows.
Example:
Order Created
↓
Reserve Inventory
↓
Process Payment
↓
Ship Product
NestJS supports Sagas through RxJS streams:
@Saga()
orderCreated = (events$: Observable<any>) => {
return events$.pipe(
ofType(OrderCreatedEvent),
map(event => new ReserveInventoryCommand(event.orderId)),
);
};
This enables orchestration across microservices and bounded contexts.
CQRS and Event Sourcing Best Practices
Keep Commands Intent-Focused
Good:
ConfirmOrderCommand
Avoid:
UpdateOrderCommand
Events Should Be Immutable
Events represent historical facts.
Never modify existing events after publication.
Separate Read and Write Models
Optimize queries independently from command processing.
Use Snapshots for Large Aggregates
Instead of replaying thousands of events:
Snapshot #5000
+ Events 5001-5010
This significantly improves performance.
Version Your Events
Example:
OrderCreatedV1
OrderCreatedV2
Event versioning simplifies system evolution.
When to Use CQRS and Event Sourcing
Good candidates:
- Financial systems
- E-commerce platforms
- Inventory management
- Audit-heavy applications
- Workflow engines
- Event-driven microservices
Avoid if:
- Simple CRUD application
- Small internal tool
- Minimal auditing requirements
- Team unfamiliar with distributed systems
CQRS and Event Sourcing add complexity, so ensure the benefits outweigh the operational costs.
Conclusion
CQRS and Event Sourcing provide powerful architectural patterns for building scalable, maintainable, and auditable systems. By separating commands from queries and storing changes as immutable events, applications gain flexibility, traceability, and the ability to reconstruct historical state at any point in time.
NestJS makes implementing these patterns straightforward through the @nestjs/cqrs package, which offers built-in support for Commands, Queries, Events, Event Handlers, Aggregates, and Sagas. While these patterns introduce additional complexity, they can dramatically improve scalability and maintainability in complex domains such as e-commerce, finance, logistics, and distributed microservices.
As your NestJS applications grow, CQRS and Event Sourcing can become valuable tools for building resilient event-driven architectures that are ready for future expansion.
You can find the full source code on our GitHub.
We know that building beautifully designed Mobile and Web Apps from scratch can be frustrating and very time-consuming. Check Envato unlimited downloads and save development and design time.
That's just the basics. If you need more deep learning about Nest.js, you can take the following cheap course:
- NestJS: The Complete Developer's Guide
- NestJS Masterclass - NodeJS Framework Backend Development
- NestJS Mastery: Build & Deploy a Production-Ready API
- NestJS Zero to Hero - Modern TypeScript Back-end Development
- NestJS Microservices: Build & Deploy a Scaleable Backend
- NestJS: Build a Real-Time Social Media API (/w WebSockets)
- NestJS Unleashed: Develop Robust and Maintainable Backends
- The Nest JS Bootcamp - Complete Developer Guide
Thanks!
