Domain Events in NestJS with Simple Example
- Pavol Megela
- Sep 25, 2022
- 4 min read
Updated: Mar 9

In the previous blog, we explored Domain-Driven Design (DDD) concepts, including Entities, Aggregates, Repositories, and Read Models. We also discussed different ways aggregates can communicate, such as using an Orchestrating Aggregate or Domain Events.
Now, I continue with the Entities we created and look at simple example. We already know that instead of making aggregates call each other directly, Domain Events allow aggregates to communicate asynchronously, reducing tight coupling and making the system more flexible and scalable.
So, we’ll walk through a simple implementation of Domain Events in a NestJS server. By the end, you’ll see how events like StockDecreasedEvent can trigger actions in other parts of the system without breaking aggregate boundaries.
Quick Recap
In the DDD introduction article, we designed our domain using Domain-Driven Design (DDD) principles. We created two key aggregates:
Product - The main entity representing an item being sold, ensuring business rules like price validation
Stock - A separate aggregate responsible for managing inventory, enforcing rules like stock levels never going under 0
So far, these aggregates operate independently, but in many cases, they need to communicate, for example, when an order is placed, Stock must be updated. Instead of making them call each other directly we can use Domain Events to handle this communication asynchronously.
Domain Events in NestJS
We want to implement the following business requirement:
When stock is updated, we want to notify other parts of the system
We want to make sure that we implement this requirement without tight coupling of Product and Stock aggregate. That's why we decide to use Domain Events, because we know that it means that instead of directly calling other services, we emit an event when stock changes, allowing other parts of the system to react independently.
Step 1: Let's create this event
// src/modules/product/domain/events/stock-updated.event.ts
export class StockUpdatedEvent implements StockEvents {
public constructor(
public readonly productId: string,
public readonly newQuantity: number
) {}
}
Step 2: Then we modify the Stock entity to create a StockUpdatedEvent whenever stock is decreased.
// src/modules/product/domain/stock.entity.ts
export class Stock {
private events: Array<StockEvents> = [];
...
public decreaseStock(amount: number): void {
const newQuantity = this.quantity - amount;
this.validateQuantity(newQuantity);
this.quantity = newQuantity;
const stockUpdatedEvent = new StockUpdatedEvent(this.productId, this.quantity);
this.events.push(stockUpdatedEvent);
}
public pullDomainEvents(): Array<StockEvents> {
const events = [...this.domainEvents];
this.domainEvents = [];
return events;
}
...
}
Step 3: Implement an Event Handler in NestJS
To react to the StockUpdatedEvent, we use NestJS’s built-in CQRS event handlers.
This event handler will be invoked whenever the event is dispatched.
// src/modules/product/application/stock-event.handler.ts
import { EventsHandler, IEventHandler } from "@nestjs/cqrs";
import { StockUpdatedEvent } from "../domain/events/stock-updated.event";
@EventsHandler(StockUpdatedEvent)
export class StockUpdatedHandler implements IEventHandler<StockUpdatedEvent> {
public handle(event: StockUpdatedEvent): void {
// Notify external services, update analytics, etc.
}
}
Step 4: Modify the Application Service to Dispatch the Event
The Application Service (not the entity) is responsible for dispatching the event using the NestJS Event Bus. Event should be dispatched only after successful Stock quantity update. Lets look how that would look like
@Injectable()
export class StockService {
constructor(
private readonly stockRepository: StockRepository,
private readonly eventBus: EventBus
) {}
public async decreaseStock(productId: string, amount: number) {
const stock = await this.stockRepository.findByProductId(productId);
stock.decreaseStock(amount);
await this.productRepository.saveStock(stock);
stock.pullDomainEvents().forEach(event => this.eventBus.publish(event));
}
}
In real world applications you might want to use transactions for atomic updates.
How it works
A customer places an order
The system decreases stock by calling Stock.decreaseStock(), which:
Updates the stock quantity.
Adds a StockUpdatedEvent to the list of domain events
The StockService retrieves the stock object, saves it, and publishes all domain events using eventBus.publish()
NestJS automatically triggers StockUpdatedHandler, which processes the event and performs any necessary actions (logging, notifying external services)
Basics of Error handling
Domain events introduce new challenges, especially around error handling. If an event fails, it shouldn't crash the system or leave data in an inconsistent state. In this article, we’ll cover best practices for handling errors in event-driven architectures using NestJS.
Catching and Logging errors in event handlers
Event handlers should never crash the entire application. If an exception occurs during event processing, it should be logged and handled gracefully.
We update our Event handler with try-catch.
@EventsHandler(StockUpdatedEvent)
export class StockUpdatedHandler implements IEventHandler<StockUpdatedEvent> {
public handle(event: StockUpdatedEvent): void {
try {
// Notify warehouse, update analytics, etc.
} catch (error) {
// Handle error: Log, Optionally retry or send to a dead-letter queue
}
}
}
Key takeaways
Prevent a single failed event from crashing your system
Help diagnose issues through proper error logging
Allow for retries or alternative error-handling strategies
What to do next?
When an event fails repeatedly it should be stored for later review instead of being lost or endlessly retried. A Dead-Letter Queue (DLQ) is a dedicated storage mechanism for failed events.
What to Store in a DLQ?
A DLQ should capture enough information to diagnose and retry failed events. Key fields to store include:
Event Payload - The original event data
Error Message - A description of why the event failed
Failure Timestamp - When the failure occurred
Retry Attempts - Number of times the event has been retried
Where to Store DLQ Events?
For a simple implementation, you can store failed events in your database (a failed_events table). For more scalable solutions, consider using message queues like:
RabbitMQ - Supports dead-letter exchanges, allowing failed messages to be automatically reprocessed later
Kafka - Enables event replay and long-term storage, making it useful for large-scale distributed systems
Event Handlers should be Idempotent
An idempotent event handler ensures that processing the same event multiple times does not cause duplicate or unintended side effects. This is important because in distributed systems the same event might be retried due to network failures, or other reasons.
To make an event handler idempotent, it should check if the event has already been processed before taking any action. This can be done by:
Storing processed events in a database (a processed_events table)
Checking if the intended action has already been performed, such as looking up a notification record before sending an email
Conclusion
We explored how to properly implement Domain Events, ensuring that aggregates communicate without direct dependencies. We also covered best practices for error handling, including catching errors in event handlers using a Dead-Letter Queue (DLQ), ensuring idempotency and implementing fallback mechanisms.
By following these principles you can build a robust, maintainable, and fault-tolerant system that leverages the full power of Domain Events while handling failures gracefully.
Comments