Using CQRS and Event Sourcing in NestJS: Build Scalable Event-Driven Applications

by Didin J. on Jun 19, 2026 Using CQRS and Event Sourcing in NestJS: Build Scalable Event-Driven Applications

Learn how to implement CQRS and Event Sourcing in NestJS with practical examples, command handlers, event handlers, projections, and best practices.

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:

  1. Creates aggregate
  2. Applies event
  3. Commits event
  4. 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:

Thanks!