In-memory store

An @InMemoryProjection keeps its read models on the heap of each instance. It suits small read models that are cheap to rebuild, and tests.

Declaring

The annotation takes the projection’s name, which is unique ignoring case and at most 255 characters. The projection is a singleton bean.

class AccountSummary(
    @ReadModelKey var accountId: AccountId = AccountId(),
    var owner: String = "",
    var balance: BigDecimal = BigDecimal.ZERO,
)

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

    @InitReadModel
    fun init(id: AccountId) = AccountSummary(accountId = id)

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

interface AccountSummaryRepository : InMemoryProjectionRepository<AccountSummary, AccountId> {
    fun findByOwner(owner: String): List<AccountSummary>
}

The framework implements the repository, so it needs no @Bean. It offers derived finders on any property of the read model, is read-only, and returns copies. Repositories are found in the application’s packages, or only in the packages @EnableProjectionRepositories names. The repository’s id type is the type of the read model’s own id; see The read model’s own id. To serve the read models over HTTP, see Read models over HTTP.

Instances and rebuilds

An in-memory projection runs on every instance, each with its own read models. It is rebuilt on every start. POST /actuator/consumers/projection/account-summary/rebuild rebuilds it on the instance you call only; see Rebuild and schema evolution.

Errors

An in-memory projection always halts on a message it cannot apply: its error handler is HaltErrorHandler, and naming another for it fails the startup. See Errors & halts.

Bounding the read models

The number of read models is unbounded by default. Bound it for every in-memory projection, or for one:

spring:
  ddd:
    cqrs:
      projection:
        in-memory:
          max-read-models: 100000      # every in-memory projection and materialization; 0, the default, sets no bound
      projections:
        account-summary:
          in-memory:
            max-read-models: 500000    # this projection only

Only a new key counts against the bound. A change that would exceed it fails the apply, and nothing is evicted. Deleting a read model frees a place.

Restrictions

  • An in-memory projection cannot be another projection’s upstream; the application does not start if one is named as such.

  • The listener runs outside any transaction: the store’s writes cannot roll back with one, so a save or delete inside an active transaction fails the apply.

  • Every read and write copies the read model through JSON, so it must survive a round trip; see Read models are JSON.