Integrating Domain Events in NestJS with DDD Principles


"Decouple your code, not your logic. Domain Events make it possible."
Domain Events are a powerful concept in Domain-Driven Design (DDD) that help you capture significant occurrences within your domain and react to them cleanly. They allow systems to remain highly cohesive internally while promoting loose coupling externally.
In this article, we'll explore how to implement Domain Events in a NestJS application using DDD principles.
Why Domain Events Matter in DDD
In complex systems, changes in one part of the domain often need to trigger actions in other parts. Without Domain Events, this can lead to:
Tight coupling
Fragile integrations
Hard-to-maintain codebases
Domain Events allow different parts of your application to react to changes without being tightly bound together.
"Systems that communicate via events become more adaptable and resilient."
Structure of Domain Events in DDD
Typically, a Domain Event should:
Represent something meaningful that happened inside the domain.
Carry only necessary data.
Be immutable.
Example: Defining a Domain Event
export class OrderPlacedEvent {
constructor(
public readonly orderId: string,
public readonly userId: string,
public readonly totalAmount: number,
) {}
}
This event represents that an order has been successfully placed.
Publishing Domain Events in NestJS
Step 1: Setup a Simple Event Bus
You can create a basic in-memory event bus or use libraries like @nestjs/cqrs
.
Simple In-Memory Event Bus Example:
@Injectable()
export class EventBus {
private handlers: { [eventName: string]: Function[] } = {};
publish(event: any) {
const handlers = this.handlers[event.constructor.name] || [];
handlers.forEach(handler => handler(event));
}
subscribe(eventName: string, handler: Function) {
if (!this.handlers[eventName]) {
this.handlers[eventName] = [];
}
this.handlers[eventName].push(handler);
}
}
Step 2: Triggering Events from Aggregates
Aggregates are perfect places to trigger domain events.
class Order {
private domainEvents: any[] = [];
placeOrder() {
// Order placement logic
this.domainEvents.push(new OrderPlacedEvent(this.id, this.userId, this.totalAmount));
}
pullDomainEvents(): any[] {
const events = this.domainEvents;
this.domainEvents = [];
return events;
}
}
Step 3: Handling Events
In NestJS, you can handle events inside services or separate event handlers.
@Injectable()
export class SendConfirmationEmailHandler {
constructor(private readonly emailService: EmailService) {}
handle(event: OrderPlacedEvent) {
this.emailService.sendOrderConfirmation(event.userId, event.orderId);
}
}
You can subscribe this handler to the event in your EventBus setup.
eventBus.subscribe('OrderPlacedEvent', (event) => sendConfirmationEmailHandler.handle(event));
Benefits of Using Domain Events
Decouples your domain logic from application side-effects.
Promotes scalability.
Improves testability.
Enables eventual consistency between systems.
"Events let you build bridges without hardwiring dependencies."
Conclusion
By integrating Domain Events inside your NestJS DDD applications, you create systems that are flexible, scalable, and highly maintainable.
Remember:
Keep events small and focused.
Publish them from your domain.
Handle them asynchronously if possible.
Start treating important domain changes as first-class citizens — not afterthoughts!
More from Codanyks:
Subscribe to my newsletter
Read articles from codanyks directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
