Skip to main content

What are Domain Events?

Domain Events represent something significant that happened in your domain. They enable loose coupling between aggregates and support event-driven architectures. The Rich Domain core library provides interfaces only - you implement the event publishing mechanism according to your needs (in-memory, message queues, event streams, etc.).
import { DomainEvent } from "@woltz/rich-domain";

type OrderConfirmedPayload = {
  customerId: string;
  total: number;
};

export class OrderConfirmedEvent extends DomainEvent<OrderConfirmedPayload> {}

Creating Events

Basic Event

Events are created by extending DomainEvent with a typed payload:
type UserCreatedPayload = {
  email: string;
};

export class UserCreatedEvent extends DomainEvent<UserCreatedPayload> {}

Event with Queue Name (Optional)

You can specify a static queueName for routing events to specific queues (useful for message queue implementations):
type SendEmailPayload = {
  to: string;
  subject: string;
  body: string;
};

export class SendEmailNotification extends DomainEvent<SendEmailPayload> {
  static readonly queueName = "notification-events";
}

Event Properties

Every domain event automatically has these properties:
PropertyTypeDescription
eventIdstringUnique identifier for this event occurrence
eventNamestringName of the event (class name)
occurredOnDateWhen the event occurred
payloadPThe event data (typed)
queueNamestringOptional queue name (static property)
const event = new UserCreatedEvent({
  email: "john@example.com",
});

console.log(event.eventId);     // "1699876543210-abc123def"
console.log(event.eventName);   // "UserCreatedEvent"
console.log(event.occurredOn);  // 2024-12-27T10:30:00.000Z
console.log(event.payload);     // { email: "john@example.com" }

Raising Events from Aggregates

Use addDomainEvent() inside aggregate methods to record events:
import { Aggregate } from "@woltz/rich-domain";

class User extends Aggregate<UserProps> {
  static create(props: Omit<UserProps, "id">): User {
    const user = new User(props);

    // Add event - it will be stored in the aggregate
    user.addDomainEvent(
      new UserCreatedEvent({
        email: user.email,
      })
    );

    return user;
  }

  activate() {
    if (this.props.status === "active") return;

    this.props.status = "active";

    this.addDomainEvent(
      new UserActivatedEvent({ userId: this.id.value })
    );
  }

  changeEmail(newEmail: string) {
    const oldEmail = this.props.email;
    this.props.email = newEmail;

    this.addDomainEvent(
      new UserEmailChangedEvent({ oldEmail, newEmail })
    );
  }
}

Event Bus Interface

Rich Domain provides the IDomainEventBus interface with two methods:
interface IDomainEventBus {
  /**
   * Publish a single domain event
   */
  publish(event: IDomainEvent): Promise<void>;

  /**
   * Publish multiple domain events
   */
  publishAll(events: IDomainEvent[]): Promise<void>;
}
You implement this interface according to your infrastructure needs.

Publishing Events

From Aggregates

After persisting changes, dispatch all uncommitted events:
import { BullMQEventBus } from "./infrastructure/event-bus";

// In your service/use case
async function createUser(data: CreateUserInput) {
  // 1. Create aggregate (events are recorded)
  const user = User.create(data);

  // 2. Save to database
  await userRepository.save(user);

  // 3. Publish all uncommitted events
  await user.dispatchAll(eventBus);
  
  // 4. Clear events from aggregate
  user.clearEvents();

  return user;
}

Direct Publishing

You can also publish events directly without aggregates:
// Single event
const event = new UserCreatedEvent({ email: "john@example.com" });
await eventBus.publish(event);

// Multiple events
const events = [
  new UserCreatedEvent({ email: "john@example.com" }),
  new SendEmailNotification({
    to: "john@example.com",
    subject: "Welcome!",
    body: "Thanks for joining!",
  }),
];
await eventBus.publishAll(events);

Managing Uncommitted Events

Aggregates and entities provide methods to manage events:
const user = User.create({ email: "john@example.com" });

// Check if there are uncommitted events
console.log(user.hasUncommittedEvents()); // true

// Get all uncommitted events
const events = user.getUncommittedEvents();
console.log(events); // [UserCreatedEvent]

// Clear all events
user.clearEvents();

console.log(user.hasUncommittedEvents()); // false

Event Serialization

Events can be serialized to JSON for storage or transmission:
const event = new UserCreatedEvent({ email: "john@example.com" });

const json = event.toJSON();
console.log(json);
// {
//   eventId: "1699876543210-abc123def",
//   eventName: "UserCreatedEvent",
//   occurredOn: "2024-12-27T10:30:00.000Z",
//   payload: {
//     email: "john@example.com"
//   }
// }

API Reference

DomainEvent

abstract class DomainEvent<P> implements IDomainEvent<P> {
  readonly eventId: string;
  readonly occurredOn: Date;
  readonly payload: P;
  static readonly queueName?: string;

  constructor(payload: P);
  get eventName(): string;
  toJSON(): object;
}

IDomainEventBus

interface IDomainEventBus {
  publish(event: IDomainEvent): Promise<void>;
  publishAll(events: IDomainEvent[]): Promise<void>;
}

Entity/Aggregate Methods

class BaseEntity {
  protected addDomainEvent(event: IDomainEvent): void;
  getUncommittedEvents(): IDomainEvent[];
  clearEvents(): void;
  hasUncommittedEvents(): boolean;
  async dispatchAll(bus: IDomainEventBus): Promise<void>;
}