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’szand 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:
| Field | Description |
|---|---|
type | The event name derived from the file name (e.g., OrderPlaced) |
aggregateId | Identifies which aggregate instance this event belongs to |
payload | The event data, validated against the Zod schema |
version | Sequential version number within the aggregate |
timestamp | When the event was recorded |
metadata | Optional 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, notPlaceOrder. - 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.
Related Concepts
- Aggregates — events define the state transitions for aggregates
- Projections — transform events into read model data
- Policies — react to events and trigger side effects