Projections

A projection derives read models from the messages an application produces and keeps them up to date: the framework delivers each message the projection listens to, calls the listener with the read model the message changes, and saves it. The application queries the read models through a repository.

This page follows one change, a deposit, from the command that causes it to the query that sees it. It then defines a projection’s own words.

One change, end to end

The deposit involves an event and a command, an aggregate with its command handler, a read model with its projection and repository, and a query.

@DomainEvent(namespace = "banking", name = "MoneyDeposited")
data class MoneyDeposited(@AggregateId val accountId: AccountId, val amount: Money)

@Command(namespace = "banking", name = "DepositMoney")
data class DepositMoney(@AggregateId val accountId: AccountId, val amount: Money)
@Entity
@Table(name = "bank_accounts")
@Aggregate(namespace = "banking")
class BankAccount(
    @EmbeddedId
    @AttributeOverride(name = "value", column = Column(name = "id", nullable = false, updatable = false))
    @AggregateId
    override var id: AccountId = AccountId(),
) : AbstractAggregateRootEntity<BankAccount, AccountId>() {

    var balance: BigDecimal = BigDecimal.ZERO

    fun deposit(amount: Money) {
        balance += amount.value
        registerEvent(MoneyDeposited(id, amount))
    }
}

interface BankAccountRepository : JpaRepository<BankAccount, AccountId>

@ApplicationService
class AccountCommandHandlers(private val accounts: BankAccountRepository) {

    @CommandHandler
    fun handle(command: DepositMoney) {
        val account = accounts.findById(command.accountId).orElseThrow()
        account.deposit(command.amount)
        accounts.save(account)
    }
}
@Entity
class AccountSummary(
    @EmbeddedId var accountId: AccountId = AccountId(),
    @Version var version: Long? = null,
    var balance: BigDecimal = BigDecimal.ZERO,
)

@JpaProjection(name = "account-summary")
class AccountSummaryProjection {

    @ProjectionListener
    fun on(event: MoneyDeposited, summary: AccountSummary) {
        summary.balance += event.amount.value
    }
}

interface AccountSummaryRepository : JpaRepository<AccountSummary, AccountId>

A @JpaProjection read model is an ordinary JPA entity with a nullable @Version; Stores compares the stores, and JPA store gives this one’s rules.

@ApplicationService
class AccountQueries(private val summaries: AccountSummaryRepository) {

    fun summaryOf(accountId: AccountId): AccountSummary? = summaries.findById(accountId).orElse(null)
}

A deposit then takes four steps:

  1. DepositMoney reaches AccountCommandHandlers, which loads the BankAccount, deposits the amount and saves the account; the deposit registers MoneyDeposited. A @CommandHandler on BankAccount itself works as well; the framework then loads and saves the account. See Command handlers on a bean, Auto-managed aggregate handling and Domain events & optimistic locking.

  2. Saving the account publishes MoneyDeposited, and the framework stores it in the capture topic in the same transaction as the account: both commit, or neither does. See The capture topic.

  3. On its next poll, AccountSummaryProjection reads the event from the capture topic. The framework loads the account’s AccountSummary, or creates it on the account’s first deposit, calls the listener, and saves the summary. The summary’s key is the event’s @AggregateId, so no key is declared. See Eventual consistency.

  4. A query through AccountSummaryRepository returns the updated summary. Until the projection has applied the deposit, which is about two seconds with the default settings, the query returns the summary as it was, or nothing before the account’s first deposit.

The same projection on an event-sourced aggregate

When BankAccount is event-sourced instead, extending EventSourcingAggregateRoot, the event store keeps MoneyDeposited and the event is not captured. AccountSummaryProjection reads it from the event store, and nothing in the projection, its read model or its repository changes. See Event-Sourcing.

Terms

read model

An object a projection keeps up to date and the application queries, one per key: one AccountSummary per account.

source

What a projection reads messages from: the event store, the capture topic or another projection’s read-model topic.

The messaging terms the section uses, topic, position, partition, high-water mark, end position, checkpoint, lag, capture topic, read-model topic and tombstone, are defined in Messaging.

In this section

Writing a projection

Listeners, keys, creating and deleting read models, metadata and JSON.

Stores

The in-memory, JPA and query stores compared, and how to choose.

Ad-hoc projections

Answering from a projection on demand, without storing anything.

Read models over HTTP

Spring Data REST and Spring MVC over projection repositories.

Sources & delivery

Where a projection reads from, the capture topic, and how each message reaches the listener.

Ordering & consistency

In which order messages are applied, and how soon a query sees a change.

Running on multiple instances

Running a projection on several instances and sharing its partitions.

Rebuild & schema evolution

Rebuilding a projection’s read models, and changing a read model without losing data.

Projections on projections

Listening to another projection’s read models.

Errors & halts

What happens when a message cannot be applied, and how to resume.