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
- Loose Coupling: Components can communicate without direct dependencies
- Extensibility: New functionality can be added by subscribing to existing events
- Cross-Domain Communication: Events can be used to communicate across bounded contexts
- Audit Trail: Events provide a natural audit trail of domain changes
- Event Sourcing: Events can be used as the source of truth for the system state
- Scalability: Asynchronous event handling allows for better scalability