Projections
What is a Projection?
A projection transforms events into read model data. As events are recorded on the write side, projections consume those events and update the corresponding view, keeping the read model in sync with the domain state.
Projections in Bounda
Projections live inside read/<view-name>/projections/ with one file per event that the view cares about. The file name matches the event it handles.
read/
order-summary/
view.ts
projections/
order-placed.ts
order-confirmed.ts
order-shipped.ts
queries/
get-order-summary.ts
Defining a Projection
Each projection file exports a project function that receives the event and a projector object for modifying the read model:
import type { Projection } from "./+types/order-placed";
export function project({ event, projector }: Projection.HandlerArgs) {
projector.insert({
id: event.aggregateId,
customerId: event.payload.customerId,
status: "placed",
total: event.payload.total,
placedAt: event.timestamp,
});
}
Projector Operations
The projector object provides operations for modifying the read model data. The exact API depends on the read adapter configured for the view.
| Operation | Description |
|---|---|
insert | Add a new record to the read model |
update | Modify an existing record |
delete | Remove a record from the read model |
Insert
Creates a new entry in the read model:
export function project({ event, projector }: Projection.HandlerArgs) {
projector.insert({
id: event.aggregateId,
email: event.payload.email,
registeredAt: event.timestamp,
});
}
Update
Modifies an existing entry. The fields provided in the object are merged with the existing record:
export function project({ event, projector }: Projection.HandlerArgs) {
projector.update({
id: event.aggregateId,
status: "confirmed",
confirmedAt: event.timestamp,
});
}
Delete
Removes an entry from the read model:
export function project({ event, projector }: Projection.HandlerArgs) {
projector.delete({ id: event.aggregateId });
}
One Projection per Event per View
Each projection file handles exactly one event type. If the order-summary view needs to react to three different events, there are three projection files:
projections/
order-placed.ts -> inserts the initial record
order-confirmed.ts -> updates the status
order-cancelled.ts -> deletes the record
This keeps each projection focused and easy to reason about.
Guidelines
- Projections should be simple data mappings. Avoid complex business logic inside a projection — that belongs in the domain layer.
- The field names used in projector operations must match the fields defined in the view’s
view.ts. - Projections are idempotent by design. Replaying the full event history through projections rebuilds the read model from scratch.
- If a view needs data from an event belonging to a different aggregate, create a projection file for that event in the view’s projections directory.