← Back to list

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

As applications grow in complexity, traditional CRUD architectures often become difficult to maintain, scale, and audit. Business…

Didin Jamaludin in JavaScript in Plain English · 2026-06-19 12:33 · 0 claps · 5.4 min read
#nestjs #cqrs #event-sourcing #scalable #event-driven
Open on Medium ↗
Wiki topics: 🏛️ · Architecture

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

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

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

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

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)); } }

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

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.

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 {}

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:

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 );

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.

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!

Originally published at https://www.djamware.com.


메타데이터
post_id
5ca11f2a9b32
slug
using-cqrs-and-event-sourcing-in-nestjs-build-scalable-event-driven-applications-djamware-5ca11f2a9b32
url
https://medium.com/@didinjamaludin/using-cqrs-and-event-sourcing-in-nestjs-build-scalable-event-driven-applications-djamware-5ca11f2a9b32
canonical_url
https://medium.com/@didinjamaludin/using-cqrs-and-event-sourcing-in-nestjs-build-scalable-event-driven-applications-djamware-5ca11f2a9b32
author_url
https://medium.com/@didinjamaludin
status
ok
fetched_at
2026-06-20 20:29:01