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 eventually consistent with the domain state.
Projections are processed asynchronously by the event dispatcher. After a command persists events, the dispatcher picks them up and runs the relevant projections. In tests, call
app.processUntilIdle()before querying the read model.
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.
Related Concepts
- Views — define the schema that projections populate
- Queries — read the data that projections write
- Events — the source of data for projections
- Eventual Consistency — how and when projections execute