Integrating Domain Events in NestJS with DDD Principles

codanykscodanyks
3 min read

"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:

0
Subscribe to my newsletter

Read articles from codanyks directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

codanyks
codanyks