DEV Community

Ewerson Vieira Nascimento
Ewerson Vieira Nascimento

Posted on

Understanding Domain Events in TypeScript: Making Events Work for You

Photo by [AltumCode](https://unsplash.com/@altumcode?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)

The Lowdown on Domain Events

When we talk about Domain-Driven Design (DDD), Domain Events serve as messengers, announcing significant changes within the system. These events act as crucial bridges, allowing different parts of your application to stay informed about relevant occurrences, fostering decoupling and maintainability.

What Are Domain Events?

Domain Events are lightweight, immutable objects representing state changes or occurrences within your application’s domain. They encapsulate essential information about what happened without carrying the weight of complex logic. Examples could range from a new user registration to an order being shipped.

Creating Domain Events in TypeScript

In TypeScript, creating a Domain Event is straightforward. Let’s consider a ProductCreatedEvent:

import EventInterface from "../../@shared/event/event.interface";

export default class ProductCreatedEvent implements EventInterface {
    dataTimeOccurred: Date;
    eventData: any;

    constructor(eventData: any) {
        this.dataTimeOccurred = new Date();
        this.eventData = eventData;
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, the event captures whatever data we want to register and the exact time it happened. It’s a simple data structure designed for communication.

Handlers and Why They Matter

Event Handlers are responsible for reacting to Domain Events. They encapsulate the logic triggered by an event. For instance, when a ProductCreatedEvent occurs, you might want to send an email or log the event.

import EventHandlerInterface from "../../../@shared/event/event-handler.interface";
import ProductCreatedEvent from "../product-created.event"

export default class SendEmailWhenProductIsCreatedHandler implements EventHandlerInterface<ProductCreatedEvent> {
    handle(event: ProductCreatedEvent): void {
        console.log("Sending email to ...");
    }
}
Enter fullscreen mode Exit fullscreen mode

Handlers keep your code modular and focused. Each handler takes care of a specific event, avoiding clutter and maintaining clarity.

Dispatching Events

Event Dispatchers act as messengers, delivering events to their respective handlers. Dispatching involves notifying interested parties that an event has occurred, triggering the associated logic. Here’s a simple event dispatcher:

import EventDispatcherInterface from "./event-dispatcher.interface";
import EventHandlerInterface from "./event-handler.interface";
import EventInterface from "./event.interface";

export default class EventDispatcher implements EventDispatcherInterface {
    private eventHandlers: { [eventName: string]: EventHandlerInterface[] } = {};

    get getEventHandlers(): { [eventName: string]: EventHandlerInterface[] } {
        return this.eventHandlers;
    }

    notify(event: EventInterface): void {
        const eventName = event.constructor.name;
        if (this.eventHandlers[eventName]) {
            this.eventHandlers[eventName].forEach((eventHandler) => {
                eventHandler.handle(event);
            });
        }
    }

    register(eventName: string, eventHandler: EventHandlerInterface<EventInterface>): void {
        if (!this.eventHandlers[eventName]) {
            this.eventHandlers[eventName] = [];
        }
        this.eventHandlers[eventName].push(eventHandler);
    }

    unregister(eventName: string, eventHandler: EventHandlerInterface<EventInterface>): void {
        if (this.eventHandlers[eventName]) {
            const index = this.eventHandlers[eventName].indexOf(eventHandler);
            if (index !== -1) {
                this.eventHandlers[eventName].splice(index, 1);
            }
        }
    }

    unregisterAll(): void {
        this.eventHandlers = {};
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the EventDispatcher maintains a collection of event handlers and is responsible for registering, unregistering and dispatching events accordingly.

Putting It All Together

Let’s bring these concepts together with a practical example:

it("Should notify all event handlers", () => {
    const eventDispatcher = new EventDispatcher();
    const eventHandler = new SendEmailWhenProductIsCreatedHandler();
    const spyEventHandler = jest.spyOn(eventHandler, "handle");

    eventDispatcher.register("ProductCreatedEvent", eventHandler);

    expect(eventDispatcher.getEventHandlers["ProductCreatedEvent"][0]).toMatchObject(eventHandler);

    const productCreatedEvent = new ProductCreatedEvent({
        name: "Product 1",
        description: "Product 1 description",
        price: 10.0
    });

    // When notify is executed SendEmailWhenProductIsCreatedHandler.handle() must be called
    eventDispatcher.notify(productCreatedEvent);

    expect(spyEventHandler).toHaveBeenCalled();
});
Enter fullscreen mode Exit fullscreen mode

In this scenario, when the ProductCreatedEvent is dispatched, the associated handler (SendEmailWhenProductIsCreatedHandler) reacts by logging message.

By embracing Domain Events, Handlers, and Dispatchers, you enhance the flexibility, maintainability, and scalability of your applications. These practices encourage a loosely coupled architecture, allowing you to adapt and extend your system more efficiently as your domain evolves.

Top comments (0)