Skip to content

Domain Events

Domain events represent something significant that has happened in the domain. They are used to communicate between different parts of the system, especially across bounded contexts.

Current State

The current codebase doesn't use domain events. Instead, it relies on direct method calls and database transactions to coordinate actions across different parts of the system. This leads to:

  • Tight coupling between components
  • Difficulty in extending the system
  • Challenges in implementing cross-domain concerns
  • Limited ability to react to domain changes

Implementing Domain Events

1. Define Domain Event Base Class

JavaScript
// src/domain/sharedKernel/events/DomainEvent.js
class DomainEvent {
  constructor() {
    this.occurredOn = new Date();
    this.eventId = crypto.randomUUID();
  }

  getEventName() {
    return this.constructor.name;
  }
}

module.exports = DomainEvent;

2. Implement Specific Domain Events

JavaScript
// src/domain/requisitionManagement/events/RequisitionCreatedEvent.js
const DomainEvent = require('../../sharedKernel/events/DomainEvent');

class RequisitionCreatedEvent extends DomainEvent {
  constructor(requisition) {
    super();
    this.requisitionId = requisition.id;
    this.createdBy = requisition.createdBy;
    this.status = requisition.status.value;
    this.type = requisition.type;
  }
}

module.exports = RequisitionCreatedEvent;
JavaScript
// src/domain/requisitionManagement/events/RequisitionSubmittedEvent.js
const DomainEvent = require('../../sharedKernel/events/DomainEvent');

class RequisitionSubmittedEvent extends DomainEvent {
  constructor(requisition) {
    super();
    this.requisitionId = requisition.id;
    this.submittedBy = requisition.createdBy;
    this.approvers = requisition.approvers.map(a => ({
      id: a.id,
      userId: a.userId,
      level: a.level
    }));
  }
}

module.exports = RequisitionSubmittedEvent;
JavaScript
// src/domain/requisitionManagement/events/RequisitionApprovedEvent.js
const DomainEvent = require('../../sharedKernel/events/DomainEvent');

class RequisitionApprovedEvent extends DomainEvent {
  constructor(requisition) {
    super();
    this.requisitionId = requisition.id;
    this.approvers = requisition.approvers.map(a => ({
      id: a.id,
      userId: a.userId,
      status: a.status,
      level: a.level
    }));
  }
}

module.exports = RequisitionApprovedEvent;
JavaScript
// src/domain/requisitionManagement/events/RequisitionRejectedEvent.js
const DomainEvent = require('../../sharedKernel/events/DomainEvent');

class RequisitionRejectedEvent extends DomainEvent {
  constructor(requisition, reason) {
    super();
    this.requisitionId = requisition.id;
    this.rejectedBy = requisition.approvers.find(a => a.status === 'REJECTED').userId;
    this.reason = reason;
  }
}

module.exports = RequisitionRejectedEvent;

3. Implement Event Bus

The event bus is responsible for publishing events and notifying subscribers:

JavaScript
// src/infrastructure/messaging/EventBus.js
class EventBus {
  constructor() {
    this.handlers = {};
    this.asyncHandlers = {};
  }

  /**
   * Subscribe to an event
   * @param {string} eventName - The name of the event
   * @param {Function} handler - The handler function
   * @param {boolean} isAsync - Whether the handler is asynchronous
   */
  subscribe(eventName, handler, isAsync = false) {
    if (isAsync) {
      if (!this.asyncHandlers[eventName]) {
        this.asyncHandlers[eventName] = [];
      }
      this.asyncHandlers[eventName].push(handler);
    } else {
      if (!this.handlers[eventName]) {
        this.handlers[eventName] = [];
      }
      this.handlers[eventName].push(handler);
    }
  }

  /**
   * Publish an event
   * @param {DomainEvent} event - The event to publish
   */
  publish(event) {
    const eventName = event.getEventName();

    // Execute synchronous handlers
    if (this.handlers[eventName]) {
      this.handlers[eventName].forEach(handler => {
        try {
          handler(event);
        } catch (error) {
          console.error(`Error in event handler for ${eventName}:`, error);
        }
      });
    }

    // Execute asynchronous handlers
    if (this.asyncHandlers[eventName]) {
      this.asyncHandlers[eventName].forEach(handler => {
        // Execute async handlers in the background
        setImmediate(async () => {
          try {
            await handler(event);
          } catch (error) {
            console.error(`Error in async event handler for ${eventName}:`, error);
          }
        });
      });
    }
  }
}

module.exports = EventBus;

4. Implement Event Handlers

Event handlers react to domain events and perform actions in response:

JavaScript
// src/application/notificationManagement/NotificationEventHandler.js
class NotificationEventHandler {
  constructor({ notificationService, userRepository }) {
    this.notificationService = notificationService;
    this.userRepository = userRepository;
  }

  /**
   * Handle RequisitionSubmittedEvent
   * @param {RequisitionSubmittedEvent} event - The event
   */
  async handleRequisitionSubmitted(event) {
    // Get approvers
    const approverIds = event.approvers.map(a => a.userId);

    // Create notifications for approvers
    for (const approverId of approverIds) {
      await this.notificationService.createNotification({
        recipientId: approverId,
        type: 'REQUISITION_APPROVAL_REQUIRED',
        content: `A new requisition #${event.requisitionId} requires your approval.`,
        referenceId: event.requisitionId,
        referenceType: 'REQUISITION'
      });
    }
  }

  /**
   * Handle RequisitionApprovedEvent
   * @param {RequisitionApprovedEvent} event - The event
   */
  async handleRequisitionApproved(event) {
    // Get requisition creator
    const requisition = await this.requisitionRepository.findById(event.requisitionId);

    // Notify creator
    await this.notificationService.createNotification({
      recipientId: requisition.createdBy,
      type: 'REQUISITION_APPROVED',
      content: `Your requisition #${event.requisitionId} has been approved.`,
      referenceId: event.requisitionId,
      referenceType: 'REQUISITION'
    });
  }

  /**
   * Handle RequisitionRejectedEvent
   * @param {RequisitionRejectedEvent} event - The event
   */
  async handleRequisitionRejected(event) {
    // Get requisition creator
    const requisition = await this.requisitionRepository.findById(event.requisitionId);

    // Notify creator
    await this.notificationService.createNotification({
      recipientId: requisition.createdBy,
      type: 'REQUISITION_REJECTED',
      content: `Your requisition #${event.requisitionId} has been rejected. Reason: ${event.reason}`,
      referenceId: event.requisitionId,
      referenceType: 'REQUISITION'
    });
  }
}

module.exports = NotificationEventHandler;

5. Register Event Handlers

Register event handlers with the event bus:

JavaScript
// src/infrastructure/config/eventHandlers.js
const NotificationEventHandler = require('../../application/notificationManagement/NotificationEventHandler');
const AuditLogEventHandler = require('../../application/auditLogManagement/AuditLogEventHandler');

function registerEventHandlers(container) {
  const eventBus = container.resolve('eventBus');

  // Notification handlers
  const notificationHandler = container.resolve('notificationEventHandler');
  eventBus.subscribe('RequisitionSubmittedEvent', notificationHandler.handleRequisitionSubmitted.bind(notificationHandler), true);
  eventBus.subscribe('RequisitionApprovedEvent', notificationHandler.handleRequisitionApproved.bind(notificationHandler), true);
  eventBus.subscribe('RequisitionRejectedEvent', notificationHandler.handleRequisitionRejected.bind(notificationHandler), true);

  // Audit log handlers
  const auditLogHandler = container.resolve('auditLogEventHandler');
  eventBus.subscribe('RequisitionCreatedEvent', auditLogHandler.handleRequisitionCreated.bind(auditLogHandler), true);
  eventBus.subscribe('RequisitionSubmittedEvent', auditLogHandler.handleRequisitionSubmitted.bind(auditLogHandler), true);
  eventBus.subscribe('RequisitionApprovedEvent', auditLogHandler.handleRequisitionApproved.bind(auditLogHandler), true);
  eventBus.subscribe('RequisitionRejectedEvent', auditLogHandler.handleRequisitionRejected.bind(auditLogHandler), true);
}

module.exports = registerEventHandlers;

6. Update Dependency Injection Container

JavaScript
// src/container.js
// Register event bus and handlers
diContainer.register({
  eventBus: asClass(require('./infrastructure/messaging/EventBus')).singleton(),
  notificationEventHandler: asClass(require('./application/notificationManagement/NotificationEventHandler')).singleton(),
  auditLogEventHandler: asClass(require('./application/auditLogManagement/AuditLogEventHandler')).singleton(),
});

// Register event handlers
const registerEventHandlers = require('./infrastructure/config/eventHandlers');
registerEventHandlers(diContainer);

Benefits of Domain Events

  1. Loose Coupling: Components can communicate without direct dependencies
  2. Extensibility: New functionality can be added by subscribing to existing events
  3. Cross-Domain Communication: Events can be used to communicate across bounded contexts
  4. Audit Trail: Events provide a natural audit trail of domain changes
  5. Event Sourcing: Events can be used as the source of truth for the system state
  6. Scalability: Asynchronous event handling allows for better scalability