/ Docs

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.

OperationDescription
insertAdd a new record to the read model
updateModify an existing record
deleteRemove 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.
  • Views — define the schema that projections populate
  • Queries — read the data that projections write
  • Events — the source of data for projections