Skip to content

Events & Event handlers

Events are classes that describe facts that have occurred within your domain. In an event-sourced system, events represent immutable pieces of historical data that record what has happened. These events are the primary means by which aggregates communicate changes in state.

Events are simple objects that represent a fact that has occurred. They are implemented by decorating the class with the @Event() decorator and by implementing the IEvent interface from the library. The decorator can take an optional name argument, which is used internally to identify the event and map it within your application.

To create an event using this library you will need to make your event class:

  • implement the IEvent interface
  • apply the @Event() decorator
import { Event, type IEvent } from '@ocoda/event-sourcing';
@Event('account-opened')
export class AccountOpenedEvent implements IEvent {
constructor(
public readonly accountId: string,
public readonly openedOn: string,
public readonly accountOwnerIds?: string[],
) {}
}

The IEvent interface is primarily for typing purposes, while the @Event() decorator marks the class as an event and optionally specifies what the name of that event should look like in the event store. For example, @Event(‘account-opened’) would create the following event-name:

Terminal window
account-opened

If the event name isn’t provided in the decorator, the name of the class will be used as is, so it isn’t converted to lowercase: @Event() on the AccountOpenedEvent class above results in the event-name AccountOpenedEvent.

Events need to be registered in the module before they can be stored and read, otherwise an UnregisteredEventException is thrown. List them in the events of EventSourcingModule.forRoot() or register them per feature module with EventSourcingModule.forFeature(). See Module configuration for more information.

An EventHandler is responsible for applying the event on the aggregate. When an event is applied to an aggregate, the aggregate looks for a method that matches the provided event. To register a method as being responsible for handling a specific event, you need to decorate the method with the @EventHandler() decorator. This only works in a class that extends AggregateRoot, see Aggregates.

import { Aggregate, AggregateRoot, EventHandler } from '@ocoda/event-sourcing';
@Aggregate({ streamName: 'account' })
export class Account extends AggregateRoot {
public balance = 0;
public credit(amount: number) {
this.applyEvent(new AccountCreditedEvent(amount));
}
@EventHandler(AccountCreditedEvent)
applyAccountCreditedEvent(event: AccountCreditedEvent) {
this.balance += event.amount;
}
}

Bear in mind this is not the same as an Event Subscriber, which is a separate concept that listens for events and reacts to them.

By default events are serialized to plain JSON-like payloads and deserialized again by JsonEventSerializer, which needs no decorators. This works well for simple events that only contain plain values. If you need more advanced events, for example containing Value Objects, it’s possible to provide your own serialization logic by creating an EventSerializer. Events that use class-transformer decorators can use ClassTransformerEventSerializer instead. For more information on how to create an Event Serializer, please refer to the Event Serialization documentation.

  1. An event should express a fact that occurred within the domain. Instead of naming events like BalanceChangedEvent, consider using domain-specific names such as AccountCreditedEvent or AccountDebitedEvent.

  2. Once an event has been created and persisted, it cannot be changed. This immutability ensures that the system can replay events to rebuild the aggregate’s state exactly as it was.

  3. Events should be simple data structures that describe what happened. Avoid adding logic to events, as this can lead to unexpected behavior when replaying events.