Configuration properties

All framework properties live under the spring.ddd.* namespace and are bound automatically via Spring Boot @ConfigurationProperties. Sensible defaults are chosen so most applications run without any explicit configuration. Durations are written in Spring Boot’s simple format: 50ms, 30s, 5m, 1h, 30d.

Each section below shows its properties as one YAML block with their defaults: ~ means unset, [] an empty list, {} an empty map. A key in angle brackets, such as <name>:, stands for a name you choose. A comment ending in min. 1 or > 0 gives the lowest value the key accepts. Where a value is limited beyond the obvious, often by another property, a Valid values table follows the block. Any invalid value fails the startup, naming the property.

How the properties are structured

  • A property sits where it belongs logically, whichever module provides it: the read-model topic’s settings sit under spring.ddd.messaging.capture.read-model, although the CQRS module provides them.

  • Storage settings sit in one jdbc group per area: spring.ddd.eventsourcing.jdbc, spring.ddd.partitioning.jdbc, spring.ddd.messaging.jdbc, spring.ddd.cqrs.jdbc and spring.ddd.saga.jdbc. Several modules may bind one group, each for its own tables; a key means the same wherever it is read. jdbc.initialize-schema: always | embedded | never (default always) says whether the area creates its tables at startup; embedded creates them only in an embedded database, e.g. H2 in tests while production uses Flyway or Liquibase. One switch covers every table of its area; the partitioning area has none, since its table is created with the event store’s and the capture topic’s.

  • jdbc.table-prefix (default empty) goes before every table name of its area, and jdbc.table-names.<table> replaces one name; <table> is the table’s default name in kebab case, consumer-checkpoints for CONSUMER_CHECKPOINTS. A name the framework derives from a class (an aggregate’s <NAME>_EVENTS and <NAME>_SNAPSHOTS, a query table named after its projection, a saga’s <NAME>_INSTANCES from @SagaTable) takes the prefix too; a name written in an annotation (@EventTable("…​"), @QueryProjection(table = "…​"), @SagaTable("…​")) is used as written. A prefix such as ACME_ keeps two applications' tables apart in one schema; the index and constraint names built from a table name take it too. A prefix is empty or starts a SQL identifier, a name is a SQL identifier, and a name with the longest index or constraint name built on it must fit in 63 characters, or the startup fails naming the properties. MySQL and SQL Server keep an index name’s case as written and compare index names case-insensitively, so a derived index name reads in mixed case, for example idx_PARTITION_MEMBERS_instance_id; this is cosmetic.

  • Defaults sit in a singular group (e.g. spring.ddd.cqrs.projection), per-name overrides in its plural sibling (e.g. spring.ddd.cqrs.projections.<name>) with the same keys; each unset key falls back to the default.

Event Sourcing

The event store, snapshots and the generated EventSourcingRepository implementations.

spring.ddd.eventsourcing:
  enabled: true                # master switch for the event-sourcing infrastructure
  validation:
    enabled: true              # run bean validation on an aggregate before persisting it
  snapshot:
    enabled: true              # create snapshots, so loading an aggregate need not replay its whole stream
    threshold: 100             # events after which a new snapshot is created
  repositories:
    enabled: true              # discover and implement EventSourcingRepository interfaces
    load:
      chunk-size: 100          # aggregate ids per query of a bulk load (findAllById); never limits the result; min. 1
  jdbc:
    initialize-schema: always  # create the event, snapshot and position tables and the partition-metadata table: always | embedded | never
    table-prefix: ""           # put before the names below and every name derived from an aggregate, e.g. ACME_; never before a name on @EventTable
    table-names:
      event-store: EVENT_STORE                                     # events of every aggregate without @EventTable
      aggregate-snapshots: AGGREGATE_SNAPSHOTS                     # snapshots of every aggregate without @EventTable
      event-store-global-position: EVENT_STORE_GLOBAL_POSITION     # a sequence on PostgreSQL, a counter table elsewhere

Valid values:

Property Valid values

jdbc.table-prefix

empty, or letters, digits and _, not starting with a digit

jdbc.table-names.*

letters, digits and _, not starting with a digit; with the prefix at most 63 characters less what the index and constraint names add (event-store: 22; aggregate-snapshots: 12; event-store-global-position: 0). A name derived from an aggregate, or written on @EventTable, is held to the same limits.

An application that declares its own EventStore bean owns that store’s schema, even when its bean wraps the framework’s JDBC store: the framework then neither creates its tables nor registers them for test cleanup. The same holds for its own SnapshotStore and the snapshot tables. See Database table schemas.

Partitioning

The partition count of the event store, which stamps every appended event with a stable bucket of its aggregate id.

spring.ddd.partitioning:
  partitions: 256   # fixed partition count; 1 means a single partition. Immutable once initialized.
  jdbc:
    table-prefix: ""                  # put before the table name below, e.g. ACME_
    table-names:
      partition-metadata: PARTITION_METADATA    # the table holding the partition count; the event store and the capture topic create it

Valid values:

Property Valid values

partitions

a power of two from 1 to 32768

jdbc.table-prefix

empty, or letters, digits and _, not starting with a digit

jdbc.table-names.partition-metadata

letters, digits and _, not starting with a digit; with the prefix at most 63 characters

The count is persisted in the partition-metadata table (PARTITION_METADATA unless configured otherwise) on first startup; every later startup fails if the configured value differs from it.

Messaging

The messaging runtime: the topic consumers and the executor they apply on, the event capture topic, and the projections' read-model topics.

spring.ddd.messaging:
  enabled: true                       # master switch for the messaging runtime: checkpoints, high-water mark,
                                      # capture, consumers. false only if the application consumes no topic.
  consumer:
    executor:
      platform:                       # used with platform threads (spring.threads.virtual.enabled=false)
        core-pool-size: 2             # threads kept while idle; min. 1
        max-pool-size: 4              # the most applies at once on this instance, across every projection
      virtual:                        # used with virtual threads (spring.threads.virtual.enabled=true)
        concurrency-limit: 4          # the most applies at once on this instance, across every projection; min. 1, or -1 = no limit
    heartbeat-interval: 5s            # how often this instance renews its membership and every partition lease it holds
    stale-instance-timeout: 30s       # how long an instance and its leases stay live without a heartbeat;
                                      # after a crash another instance takes over once it lapses
    rebalance-interval: 10s           # how often this instance claims, revokes and releases partitions
    graceful-shutdown-timeout: 20s    # how long a stopping consumer or high-water-mark detector waits for its poll in flight.
                                      # > 0; keep it below spring.lifecycle.timeout-per-shutdown-phase.
    liveness-warn-interval: 5m        # how often a stuck source or a failing lease claim repeats its warning; > 0
    retry-dead-letters-on-start: false  # each start of a consumer retries its DEAD_LETTER_KEY and DEAD_LETTER dead letters
                                      # with a fresh budget, whatever its error handler is now;
                                      # a dead-lettered key from its first, its later messages in order behind it
    delivery-retry:                   # retrying an undelivered message on later polls; the budget survives restarts and lease moves;
                                      # an error handler's own retryPolicy replaces it; past it, the error handler decides
      policy: exponential             # the backoff shape: fixed | linear | exponential; the other groups are ignored
      max-retries: 5                  # retries after the first attempt; min. 0
      jitter: 0s                      # spreads each delay so instances do not retry in lockstep; min. 0
      fixed:
        delay: 5s                     # the delay of every attempt; > 0
      linear:
        initial-delay: 2s             # the first delay; > 0
        increment: 2s                 # what each further attempt adds; > 0
        max-delay: 30s                # the ceiling of every delay; > 0
      exponential:
        initial-delay: 2s             # the first delay; > 0
        max-delay: 30s                # the ceiling of every delay
        multiplier: 2.0               # what each delay multiplies the one before by; 1.0 = constant; min. 1.0
      include-exceptions: []          # fully qualified class names of the only causes retried; empty = every cause
      exclude-exceptions: []          # causes never retried; wins over include-exceptions
    conflict-retry:                   # retrying a write conflict in place, inside the apply, with exponential backoff
      max-retries: 3                  # retries before the conflict becomes an ordinary failure for delivery-retry; min. 0
      initial-delay: 50ms             # the first delay; > 0
      max-delay: 1s                   # the ceiling of every delay
      jitter: 25ms                    # spreads each delay so instances contending for one row do not retry in lockstep; min. 0
  capture:
    event:
      enabled: true                   # capture events into the capture topic; false registers no capture bean or table
      exclude-event-types: []         # event types never captured, by canonical name or fully qualified class name
      retention:                      # what may be deleted from the capture topic; nothing while both limits are unset
        max-age: ~                    # messages that occurred longer ago than this may be deleted; > 0
        max-messages: ~               # at most this many of the newest messages are kept; with max-age, either applies; min. 1
        schedule:
          enabled: true               # this instance runs a set policy on its own; false = only runs the application starts
          interval: ~                 # delay between the end of one run and the start of the next; unset (and no cron) = 1h; > 0
          cron: "-"                   # Spring cron expression in the server's time zone; "-" or blank = not set
    read-model:
      enabled: true                   # write each persistent projection's read-model topic; with false none is written,
                                      # and a projection another listens to, a @QueryProjection or a read-model route
                                      # fails the startup
      compaction:                     # deletes each read model's superseded messages; always on
        schedule:
          enabled: true               # this instance compacts; off on every instance, superseded messages pile up
          interval: ~                 # delay between runs; unset (and no cron) = 1m; > 0
          cron: "-"                   # Spring cron expression in the server's time zone; "-" or blank = not set
      retention:                      # which tombstones may be deleted; nothing while max-age is unset
        tombstones:
          max-age: ~                  # a tombstone older than this is deleted once compaction and every consumer of its topic passed it
        include: []                   # read-model types retention applies to; empty = every one with a read-model topic
        exclude: []                   # read-model types retention leaves alone; applied after include
        schedule:
          enabled: true               # this instance runs tombstone retention
          interval: ~                 # delay between runs; unset (and no cron) = 1h; > 0
          cron: "-"                   # Spring cron expression in the server's time zone; "-" or blank = not set
  jdbc:
    initialize-schema: always         # create every messaging table at startup: the consumer state and coordination tables,
                                      # the capture tables with the partition-metadata table (while event capture is
                                      # enabled), the high-water-mark table, and the read-model topics' tables in every
                                      # datasource holding one; on PostgreSQL the high-water-mark table too in each of
                                      # those datasources
    table-prefix: ""                  # put before every table name below, e.g. ACME_; the partition-metadata table takes
                                      # spring.ddd.partitioning.jdbc
    table-names:
      consumers: CONSUMERS
      consumer-checkpoints: CONSUMER_CHECKPOINTS
      consumer-undelivered-messages: CONSUMER_UNDELIVERED_MESSAGES
      consumer-skipped-messages: CONSUMER_SKIPPED_MESSAGES
      partition-ownership: PARTITION_OWNERSHIP
      partition-members: PARTITION_MEMBERS
      partition-instances: PARTITION_INSTANCES
      event-capture-records: EVENT_CAPTURE_RECORDS
      event-capture-global-position: EVENT_CAPTURE_GLOBAL_POSITION   # a sequence on PostgreSQL, a counter table elsewhere
      high-water-mark-store: HIGH_WATER_MARK_STORE
      read-model-capture-records: READ_MODEL_CAPTURE_RECORDS
      read-model-capture-positions: READ_MODEL_CAPTURE_POSITIONS     # a sequence on PostgreSQL, a counter table elsewhere
      topic-compaction: TOPIC_COMPACTION

With enabled: false the event store still appends events and loads aggregates, and events are still encoded and upcast; only the topics consumers read are not wired. Each subsystem may also be switched off on its own: capture.event.enabled for capture, spring.ddd.messaging.high-water-mark.enabled for the detector.

Which executor group applies depends on spring.threads.virtual.enabled: consumer.executor.platform with platform threads, consumer.executor.virtual with virtual threads. The other group is ignored, never validated. Each apply holds a database connection while it runs, and each projection holds one more while it reads its sources, so size the connection pool for the sum of both; -1 leaves the applies unbounded while the pool stays fixed. A conflict retry’s backoff sleeps inside the apply and keeps its thread or its place under the limit while it waits, so a storm of write conflicts across projections can occupy them all.

Valid values:

Property Valid values

consumer.executor.platform.max-pool-size

at least core-pool-size

consumer.heartbeat-interval

positive, below half of stale-instance-timeout

consumer.stale-instance-timeout

at least 1s (the database computes expiries in whole seconds)

consumer.rebalance-interval

positive, below stale-instance-timeout

consumer.delivery-retry.exponential.max-delay

positive, at least initial-delay; with policy: exponential and a non-zero jitter, strictly above initial-delay

consumer.conflict-retry.max-delay

positive, at least initial-delay, strictly above it with a non-zero jitter; below consumer.stale-instance-timeout; max-retries × max-delay stays below consumer.graceful-shutdown-timeout

capture.event.retention.schedule.cron

a valid cron expression; not together with interval

capture.read-model.compaction.schedule.cron

a valid cron expression; not together with interval

capture.read-model.retention.tombstones.max-age

positive, longer than the longest an in-memory projection lags behind (its position is not stored)

capture.read-model.retention.include, capture.read-model.retention.exclude

fully-qualified binary names of projections' read-model types with a read-model topic; a nested class as Outer$Inner, quoted in YAML

capture.read-model.retention.schedule.cron

a valid cron expression; not together with interval

jdbc.table-prefix

empty, or letters, digits and _, not starting with a digit

jdbc.table-names.*

letters, digits and _, not starting with a digit; with the prefix at most 63 characters less what the index and constraint names add (partition-ownership, partition-members: 16; consumer-skipped-messages: 13; consumer-checkpoints, consumer-undelivered-messages: 12; consumers: 3; event-capture-records: 19; read-model-capture-records: 14)

A name in capture.event.exclude-event-types that resolves to a command or query fails the startup; a name matching nothing is logged as a warning.

Capture retention never passes the slowest subscribed consumer of the capture topic, nor its high-water mark; the event store itself has no retention, since it is the aggregates' source of truth. Every instance runs its own cleanup schedules, which is safe because a second run deletes nothing; turn them off on all but one instance to save the repeated work. An application that declares its own CheckpointStore, PartitionOwnershipStore, PartitionMemberStore or CaptureTopicStorage bean owns that store’s schema: the framework then neither creates its tables nor registers them for test cleanup. See Capture topic retention, Running on multiple instances and Database table schemas.

High-water mark

The detector that advances every topic’s high-water mark: its gapless, commit-safe end, where a reader stops so that a message whose transaction has not ended yet is waited for rather than skipped. One instance at a time holds a topic’s lease and advances its mark; a topic whose cycles fail or hang delays no other topic.

spring.ddd.messaging.high-water-mark:
  enabled: true                # run the detector; false stops every consumer. Only for an application that consumes no topic.
  polling:
    interval: 1s               # delay between detector cycles, and so how soon a committed event becomes readable
  lease-ttl: 1m                # lifetime of a topic's lease; a crashed lease holder is replaced after at most this long

Valid values:

Property Valid values

polling.interval

positive, shorter than lease-ttl

lease-ttl

at least 1s

Its table is named and created under spring.ddd.messaging.jdbc.

With spring.threads.virtual.enabled=true, the framework’s consumers, their applies and the high-water-mark detector run on virtual threads; spring.ddd.messaging.consumer.executor.virtual.concurrency-limit bounds the applies.

CQRS

The command and query pipelines, and the tables of the projections' rebuild and the query projection.

spring.ddd.cqrs:
  enabled: true      # master switch for the CQRS infrastructure
  command:
    enabled: true    # the CommandBus and @CommandHandler dispatch
  query:
    enabled: true    # the QueryBus and @QueryHandler dispatch
  jdbc:
    initialize-schema: always  # create the epochs table in each datasource holding a persistent projection's read models,
                               # and each @QueryProjection's query table and @QueryField indexes: always | embedded | never
    table-prefix: ""           # put before the name below and every query table named after its projection, e.g. ACME_;
                               # never before @QueryProjection(table = ...)
    table-names:
      read-model-epochs: READ_MODEL_EPOCHS

Valid values:

Property Valid values

jdbc.table-prefix

empty, or letters, digits and _, not starting with a digit

jdbc.table-names.read-model-epochs

letters, digits and _, not starting with a digit; with the prefix at most 60 characters

An existing query table is never altered, and is checked against the declaration either way; where the tables are not created, a missing one fails the startup.

CQRS projections

The defaults every projection runs with.

spring.ddd.cqrs.projection:
  polling:
    interval: 1s       # how long a caught-up projection waits before it reads its sources again;
                       # a change is visible within this plus the high-water-mark polling interval; > 0
    batch-size: 500    # positions read per poll; smaller means more reads, less memory, faster partition handover; min. 1
  concurrency-limit: -1      # the most of a projection's messages applied at once on this instance; min. 1, or -1 = no
                             # limit of its own. Only lowers the executor's limit, never raises it.
  distribution: partitioned  # how instances share a projection: partitioned | single-owner;
                             # in-memory projections run on every instance regardless
  error-handler: de.dwittkoetter.ddd.messaging.consumer.HaltErrorHandler
                             # the ErrorHandler class that decides a message a projection cannot apply within its
                             # delivery budget, resolved to its one bean; an @InMemoryProjection always halts
  on-declaration-change: warn  # what a start does with a projection whose declaration changed since its read models
                               # were derived: warn | rebuild | halt | fail
  cascade:
    max-depth: 32      # how many applies a chain of projections listening to each other's read models may run through; min. 1

A projection whose change would go past cascade.max-depth stops: the change commits nothing, is delivered again, and the projection holds there, its failure naming the projections the chain cycles through. Only a chain whose read models keep changing reaches it; an apply that changes nothing ends the chain.

CQRS projections by name

Settings of one projection, keyed by its name; each unset key falls back to its default under spring.ddd.cqrs.projection.

spring.ddd.cqrs.projections:
  <name>:                    # e.g. account-summary
    polling:
      interval: ~            # the projection's poll interval; > 0
      batch-size: ~          # the projection's positions per poll; min. 1
    concurrency-limit: ~     # the most of its messages applied at once on this instance; min. 1, or -1 = no limit of its own
    distribution: ~          # how instances share it: partitioned | single-owner
    error-handler: ~         # the ErrorHandler class that decides a message it cannot apply; beats its
                             # @Projection's errorHandler
    on-declaration-change: ~ # what a start does when its declaration changed: warn | rebuild | halt | fail
    in-memory:
      max-read-models: ~     # the most read models this in-memory projection, or one materialization of it,
                             # holds; 0 = no bound; min. 0

Valid values:

Property Valid values

<name>.distribution

unset for an @InMemoryProjection, which runs on every instance; otherwise the startup fails naming the projection

<name>.error-handler, spring.ddd.cqrs.projection.error-handler

the fully qualified name of an ErrorHandler class with exactly one bean in the context, else the startup fails naming the projection and the class; an @InMemoryProjection takes HaltErrorHandler only, and its own <name>.error-handler naming another fails the startup

<name>.on-declaration-change, spring.ddd.cqrs.projection.on-declaration-change

warn, rebuild, halt or fail, ignoring case; an @InMemoryProjection takes none

partitioned spreads the projection’s partitions over the instances, each read model’s messages applied in order. single-owner applies it on one instance at a time, one message at a time, in position order across all its read models; nothing orders one topic against another. Switching keeps the projection’s progress. concurrency-limit: 1 applies the projection one message at a time in position order on each instance; use distribution: single-owner when the read model needs one order rather than one order per read model.

The built-ins in de.dwittkoetter.ddd.messaging.consumer are HaltErrorHandler, SkipErrorHandler, DeadLetterKeyErrorHandler and DeadLetterErrorHandler. HaltErrorHandler stops the projection just before a message it cannot apply within its delivery budget, on every instance, until it is resumed or restarted; SkipErrorHandler passes over the message, which is then missing from the read model until it is rebuilt. A projection’s own error-handler beats the errorHandler of its store annotation, which beats the default. See Configuring the error handler.

on-declaration-change decides what a start does with a projection whose listeners, keys, read-model shape, or store and table changed since its read models were derived: warn advises a rebuild, rebuild rebuilds it, halt halts it until it is rebuilt or resumed, fail fails the startup. A projection’s own setting beats the default. See When a projection’s declaration changes.

Write a name containing dots in brackets: spring.ddd.cqrs.projections.[banking.account-summary].distribution. A name is matched ignoring case, since environment variables arrive lower-cased; an environment variable cannot carry a name’s dashes, so set such a name in a properties or YAML file. A key that names no registered projection is logged as a warning at startup. See Distribution.

CQRS in-memory projections

The settings every @InMemoryProjection shares, which also bound each materialization of an ad-hoc projection; spring.ddd.cqrs.projections.<name>.in-memory.max-read-models overrides the bound for one projection.

spring.ddd.cqrs.projection.in-memory:
  max-read-models: 0   # the most read models one in-memory projection holds; 0 = no bound.
                       # Set it to keep a projection whose keys keep growing from exhausting the heap. Min. 0.

Only a read model for a new key counts: a change that would add one past the bound fails with InMemoryReadModelLimitExceededException, changes to read models already held always apply, nothing is evicted, and deleting a read model frees its place. Threads adding new keys at once can exceed the bound by up to their number. A materialization past the bound fails with the same exception and returns nothing; see Ad-hoc projections.

CQRS JPA projections

The @JpaProjection store.

spring.ddd.cqrs.projection.jpa:
  enabled: true   # configure the @JpaProjection store; false keeps the JPA aggregates, and a @JpaProjection fails the startup

CQRS query projections (experimental)

The @QueryProjection store.

The query projection (@QueryProjection, module spring-ddd-cqrs-jdbc-query) is experimental: these settings, and the projection itself, may change or be removed in a future release.
spring.ddd.cqrs.projection.query:
  enabled: true                # configure the @QueryProjection store; false makes a @QueryProjection fail the startup.
                               # Not the query pipeline: that is spring.ddd.cqrs.query.enabled.

Its query tables are named and created under spring.ddd.cqrs.jdbc. A `@JpaProjection’s tables are always the application’s. See Database table schemas.

Saga

The saga runtime and its tables.

spring.ddd.saga:
  enabled: true                # master switch for the saga runtime
  jdbc:
    initialize-schema: always  # create the saga tables: always | embedded | never
    table-prefix: ""           # put before the names below and every name derived from a saga class, e.g. ACME_; never before a name on @SagaTable
    table-names:
      saga-instances: SAGA_INSTANCES                       # instances of every saga without @SagaTable
      saga-deadlines: SAGA_DEADLINES                       # scheduled deadlines
      saga-deadline-locks: SAGA_DEADLINE_LOCKS             # the turn to deliver deadlines

Valid values:

Property Valid values

jdbc.table-prefix

empty, or letters, digits and _, not starting with a digit

jdbc.table-names.*

letters, digits and _, not starting with a digit; with the prefix at most 63 characters less what the index names add (saga-deadlines: 11; the others: 0). A name derived from a saga class with @SagaTable, prefix included, or written on it, is held to 63 characters.

An application that declares its own SagaStore bean owns that store’s schema, even when its bean wraps the framework’s JDBC store: the framework then neither creates its tables nor registers them for test cleanup.

Saga deadlines

Delivers due saga deadlines, one instance at a time.

spring.ddd.saga.deadline:
  enabled: true                         # deliver due deadlines
  polling:
    interval: 30s                       # how often to poll for due deadlines; > 0
    batch-size: 100                     # due deadlines read per poll; min. 1
  lease-ttl: 2m                         # how long an instance holds the turn to deliver deadlines without renewing it

Valid values:

Property Valid values

lease-ttl

positive, longer than polling.interval

An application that declares its own SagaDeadlineStore bean owns that store’s schema: the framework then neither creates its tables nor registers them for test cleanup. The same holds for its own SagaDeadlineLease and the deadline-lock table.

Saga consumer

The topic consumer of each saga type; see Event delivery.

spring.ddd.saga.consumer:
  start: latest                # where a new saga type starts reading: latest | earliest
  polling:
    interval: 1s               # how long a caught-up saga consumer waits before it reads its topics again; > 0
  distribution: partitioned    # how instances share a saga type: partitioned | single-owner (every-instance is rejected)
  error-handler: de.dwittkoetter.ddd.messaging.consumer.HaltErrorHandler
                               # the ErrorHandler class that decides a message a saga cannot apply, resolved to its one bean
  start-timeout: 1m            # how long a follow-up waits for its saga's start from another topic; 0 skips at once

Saga types

Settings of one saga type, keyed by its @Saga(type) id; a type with a dot in its id is written in brackets, "[payment.saga]". Every key is optional and falls back to the global key named in its comment; a key naming no saga type fails the startup. Environment variables cannot spell a saga type’s letter case or dots; set these keys in a properties or YAML file, or in SPRING_APPLICATION_JSON.

spring.ddd.sagas:
  <saga-type>:
    consumer:
      start: ~                 # unset = spring.ddd.saga.consumer.start
      polling:
        interval: ~            # unset = spring.ddd.saga.consumer.polling.interval; > 0
      distribution: ~          # unset = spring.ddd.saga.consumer.distribution; partitioned | single-owner
      error-handler: ~         # unset = spring.ddd.saga.consumer.error-handler; an ErrorHandler class
      start-timeout: ~         # unset = spring.ddd.saga.consumer.start-timeout; 0 skips at once, not negative
    retention:
      max-age: ~               # unset = spring.ddd.saga.retention.max-age; > 0. Sets retention active for this type

Saga retention

Deletes completed sagas whose completion is older than a configurable age. See Saga Retention for the guide.

spring.ddd.saga.retention:
  max-age: ~         # completed sagas older than this are deleted; > 0. Retention is active once this or a type's max-age is set
  chunk-size: 100    # sagas deleted per statement: @Min(1) @Max(1000); a run deletes chunk by chunk
  schedule:
    enabled: true    # this instance runs retention; takes no lease
    interval: ~      # fixed delay between runs; unset (and no cron) = 1h; > 0
    cron: "-"        # Spring cron expression driving retention; "-" or blank = not set

Valid values:

Property Valid values

schedule.cron

-, blank, or a valid cron expression that matches a future time; not together with interval

An application that declares its own SagaStore and sets a max-age, global or for a type, fails the startup unless it also supplies its own SagaRetentionStore.

Routing

Routes of commands, queries and events to destinations, and the route consumers that deliver events to them. Map keys are names you choose (rulesets, destinations) or a message’s canonical name: <namespace>.<name> of its @Command, @Query or @DomainEvent, or its fully-qualified class name when it declares no namespaced name; see Routing.

spring.ddd.routing:
  rulesets:
    <name>:
      enabled: true              # the @Router("<name>") bean carrying this ruleset
  destinations:
    <name>:
      transport: ~               # transport id; must match a registered command, query or event dispatcher
      url: ~                     # transport-specific address, interpreted by the dispatcher
      target: ~                  # broker target (topic, exchange) events are sent to; unset = the destination name
  commands:
    <message-name>: ~            # static command route: the destination name
  queries:
    <message-name>: ~            # static query route: the destination name
  events:
    <message-name>:
      destinations: []           # destinations the event type fans out to; unions with annotation routes
      key: ~                     # route key: a #{…} expression on the event, or a literal; unset = the message key.
                                 # A key annotated on the type's hierarchy wins.
  headers: []                    # metadata entries sent with a routed message as transport headers; empty = none
  consumer:                      # every route consumer, unless its destination overrides it
    start:
      events: latest             # where a destination starts on an event topic without a checkpoint: latest | earliest
      read-models: earliest      # where it starts on a read-model topic without a checkpoint: earliest | latest
    polling:
      interval: 1s               # how long a caught-up route consumer waits before it reads its topics again; > 0
    distribution: partitioned    # how instances share a route consumer: partitioned | single-owner
    error-handler: de.dwittkoetter.ddd.messaging.consumer.HaltErrorHandler
                                 # the ErrorHandler class that decides an event a route consumer cannot send
                                 # within its delivery budget, resolved to its one bean
  consumers:
    <destination>:               # this destination's overrides; each unset key uses consumer's
      start:
        events: ~
        read-models: ~
      polling:
        interval: ~  # > 0
      distribution: ~
      error-handler: ~

Valid values:

Property Valid values

destinations.<name>.transport

not blank; names a registered dispatcher (checked at startup)

consumers.<destination>

a destination with a route consumer; a name containing dots written in brackets

consumer.error-handler, consumers.<destination>.error-handler

the fully qualified name of an ErrorHandler class with exactly one bean in the context, else the startup fails naming the destination and the class

earliest on a read-model topic sends every read model’s latest state, then every change; latest sends only what is stored after the destination’s first start. A start position is read once per destination and topic and never moves an existing checkpoint. partitioned spreads a route consumer’s partitions over the instances under leases; single-owner runs it on one instance at a time, in order per topic. HaltErrorHandler stops a destination’s route consumer just before an event it cannot send within its delivery budget, until it is resumed or the application restarts; SkipErrorHandler passes over the event, which is never sent to that destination; see route consumer errors.

Consumer health and metrics

The consumers health indicator and the consumer.* meters of the observability starter; see Observability. Types and names are a consumer’s: projection and the projection’s name, or route and the destination’s name.

spring.ddd.metric:
  consumer:
    max-age: 10s                 # how long health and metrics reuse one read of the consumers; 0 = every call; >= 0
    lag-threshold: ~             # lag over which a running consumer's health is DOWN; unset = lag never is; > 0
  consumers:
    <type>:
      <name>:
        lag-threshold: ~         # this consumer's own lag threshold; unset = the default; > 0

The topic.* meters of the observability starter; see Observability.

spring.ddd.metric:
  topic:
    max-age: 1m                  # how long the topic gauges reuse one read of the topics; 0 = every gauge read; >= 0

Trace propagation

Writing the trace context into messages, with the observability starter; see Trace context.

spring.ddd.trace:
  propagate: false             # write the current trace context into the metadata of every message the application dispatches or appends

Spring Modulith properties

The following properties are not defined by Spring DDD. They are owned by Spring Modulith and the framework honors them because it integrates with the Modulith event-publication store. The defaults shown are Spring Modulith’s; consult the Spring Modulith Integration page and the upstream documentation for their full meaning.

spring.modulith.events:
  completion-mode: update                         # how Modulith marks an event publication complete
  republish-outstanding-events-on-restart: false  # replay incomplete event publications on restart
  externalization:
    mode: module-listener                         # which events are forwarded to an external message broker
  jdbc:
    schema-initialization:
      enabled: false                              # create the Modulith event-publication tables at startup
    schema: ~                                     # schema name of the Modulith event-publication tables