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:
-
DepositMoneyreachesAccountCommandHandlers, which loads theBankAccount, deposits the amount and saves the account; the deposit registersMoneyDeposited. A@CommandHandleronBankAccountitself 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. -
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. -
On its next poll,
AccountSummaryProjectionreads the event from the capture topic. The framework loads the account’sAccountSummary, 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. -
A query through
AccountSummaryRepositoryreturns 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 |
Terms
- read model
-
An object a projection keeps up to date and the application queries, one per key: one
AccountSummaryper 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.