/ Docs

Events

What is an Event?

An event is an immutable fact representing something that happened in the domain. Events are the source of truth in an event-sourced system — once recorded, they are never modified or deleted. Every state change in the system originates from an event.

Events in Bounda

Event files live at the aggregate root level inside domain/<aggregate>/. They are not placed inside the commands/ directory.

domain/
  order/
    order-placed.ts
    order-confirmed.ts
    order-shipped.ts
    commands/
      place-order.ts

Each event file exports two things:

  • payload — a function that receives Zod’s z and returns a schema describing the event data.
  • apply — a pure function that transitions the aggregate state.

Defining an Event

import type { Event } from "./+types/user-registered";

export const payload: Event.PayloadFunction = ({ z }) =>
  z.object({ email: z.string().email() });

export function apply({ state, event }: Event.ApplierArgs) {
  return { ...state, email: event.payload.email };
}

The payload schema validates event data at creation time, ensuring that only well-formed events enter the event store.

The apply function is pure: it takes the current state and the event, and returns the next state. It must not perform side effects or async operations.

Event Structure

Every event in Bounda carries the following data:

FieldDescription
typeThe event name derived from the file name (e.g., OrderPlaced)
aggregateIdIdentifies which aggregate instance this event belongs to
payloadThe event data, validated against the Zod schema
versionSequential version number within the aggregate
timestampWhen the event was recorded
metadataOptional contextual data (correlationId, causationId, etc.)

Typed Event Builders

Bounda’s code generation creates typed event builders from your event definitions. Inside command handlers, these builders are available on the events object:

export async function handler({ command, events }: Command.HandlerArgs) {
  return events.orderPlaced({
    aggregateId: command.aggregateId,
    customerId: command.payload.customerId,
    items: command.payload.items,
    total: command.payload.total,
  });
}

The builder name is the camelCase form of the event file name. TypeScript enforces that the payload matches the event’s Zod schema, catching errors at compile time.

The Apply Function

The apply function defines how an event changes the aggregate state. It receives state (the current aggregate state) and event (the full event object including payload):

export function apply({ state, event }: Event.ApplierArgs) {
  return {
    ...state,
    status: "confirmed",
    confirmedAt: event.timestamp,
  };
}

Note: Always return a new state object rather than mutating the existing one. The apply function should be deterministic — given the same state and event, it must always produce the same result.

Guidelines

  • Name events in past tense to reflect that they describe something that already happened: OrderPlaced, not PlaceOrder.
  • Keep event payloads minimal. Include only the data needed to reconstruct state and inform downstream consumers.
  • Never add logic or branching to apply. It should be a straightforward state mapping.
  • Events are the contract between the write side and the read side. Changing an event’s payload schema affects projections, policies, and process managers that consume it.
  • Aggregates — events define the state transitions for aggregates
  • Projections — transform events into read model data
  • Policies — react to events and trigger side effects