Sources and delivery

A projection reads its messages from a source, and which source depends on the type its listener takes. This page covers the three sources, the startup checks on captured events, and the guarantees with which a message reaches a projection or a handler.

Three sources

The type a listener takes decides its source: an event an event-sourced aggregate stores comes from the event store, any other @DomainEvent from the capture topic, and another projection’s read-model type from that projection’s read-model topic. A listener on an interface that is not sealed reads both the event store and the capture topic.

  • The event store holds the events of event-sourced aggregates; see Event-Sourcing.

  • The capture topic holds every other domain event the application publishes; see The capture topic.

  • A read-model topic holds the changes of one projection’s read models; see Projections on projections.

How a change reaches a read model: DepositMoney saves the BankAccount and captures MoneyDeposited in one transaction; AccountSummaryProjection reads it from the capture topic on its next poll, or from the event store when the account is event-sourced, applies it to the AccountSummary a query reads and writes the summary’s new state to its read-model topic for downstream projections and routes; a @DomainEventHandler receives the event in the JVM after commit, best effort

Projections on captured events

A projection listener on an event type that no event-sourced aggregate stores reads it from the capture topic. Startup fails, naming the projection and the event type, when that listener could never be called:

  • the type is not annotated @DomainEvent, so it is never captured;

  • the type is excluded from capture, by @NotCaptured on the type or by name in spring.ddd.messaging.capture.event.exclude-event-types;

  • the application has no capture topic, for instance because spring.ddd.messaging.capture.event.enabled is false.

A listener on a sealed type handles each of its subtypes, so one excluded subtype fails the startup too. A listener on a type that is not sealed also receives its subtypes, whatever the topics carry of them; an excluded subtype is never delivered to it, and startup cannot name it.

Two delivery paths

A projection reads a durable topic: it receives every message its sources still hold at least once, resumes from its checkpoint on any instance and after any restart, and can be rebuilt from its sources; see Delivery guarantees.

A projection is applied exactly once when its read-model change commits with its checkpoint: the framework keeps checkpoints in the application’s primary datasource, and the projection’s store must live there and be written by the application’s transaction manager; for a @JpaProjection, by the persistence unit of that transaction manager. Any other projection commits each change on its own, ahead of its checkpoint, so after a crash it can apply some messages again. Its listeners must give the same result when a message is applied twice. An @InMemoryProjection keeps its checkpoints in memory and is rebuilt on every start.

A @DomainEventHandler receives the event as an application event in the JVM, after the publishing transaction commits. That delivery is best effort: a handler that fails is not retried, an event in flight when the application stops is lost, and a handler is neither replayed nor rebuilt. See Delivery guarantees for durable delivery.

An event that both a handler and a projection consume travels both paths, each with its own guarantee. The split is deliberate: a side effect, such as an e-mail or a call to another system, belongs in a handler, and state the application queries belongs in a projection that can be rebuilt.

Retention and rebuilds

Retention is a trade. For an event no event-sourced aggregate stores, the capture topic is the only copy of its history, and pruning it gives up complete rebuilds of the projections it feeds: a rebuild re-derives only what the topic still holds, keeps the read models it cannot re-create, and marks the projection as built from incomplete history. Keep everything, the default, for the projections you may need to rebuild.

How long the capture topic keeps its history, what holds retention back, and the incomplete-history mark are described in Retention.