Actuators

Spring DDD integrates with Spring Boot Actuator and Micrometer to make event-store, projection, and saga state observable at runtime. Observability is entirely opt-in: three separate add-on starters each contribute one actuator endpoint, one health indicator, and a set of Micrometer metrics. Metrics flow automatically to whatever Micrometer registry the application configures — Prometheus, Datadog, or any other — without additional setup.

Prerequisites

The three actuator starters are add-on modules; none of them is bundled into spring-ddd-starter-jdbc or spring-ddd-starter-jpa. Each starter requires Spring Boot Actuator on the classpath:

implementation("org.springframework.boot:spring-boot-starter-actuator")

Web endpoints must be exposed explicitly. For example, to enable all three:

management.endpoints.web.exposure.include=eventstore,projections,sagas

Health indicators appear automatically under /actuator/health once the corresponding starter is on the classpath — no extra exposure configuration is needed.

Event store

Add spring-ddd-starter-eventsourcing-actuator:

implementation("de.dwittkoetter:spring-ddd-starter-eventsourcing-actuator:0.0.1-SNAPSHOT")

Endpoint — eventstore

The endpoint is read-only and is only registered when an EventStorePartitionStats bean is present on the classpath (provided by spring-ddd-eventsourcing-jdbc; the health indicator and metrics work without it).

Operation URL Response

GET

/actuator/eventstore

{ position, aggregates: [{ type, descriptors }] } — global position and a list of all known aggregate types.

GET

/actuator/eventstore/{type}

{ type, descriptors, eventCount, latestPosition, metrics } — per-type statistics; 404 when the type is unknown.

Health — eventStore

The component appears under /actuator/health/eventStore.

  • UP — includes a position detail with the current maximum position from the event store.

  • DOWN — if querying maxPosition() throws an exception, the exception detail is included in the response.

Metrics

Meter Type Tags Meaning

eventstore.position

Gauge

Global maximum event-store position (high-water mark).

eventstore.appends

Counter

aggregate.type (e.g. bank-account)

Number of events appended per aggregate type.

eventstore.append.latency

Timer

aggregate.type

Duration of each appendEvents call.

eventstore.append.conflicts

Counter

aggregate.type

Number of optimistic-lock conflicts during append.

Projections

Add spring-ddd-starter-cqrs-actuator:

implementation("de.dwittkoetter:spring-ddd-starter-cqrs-actuator:0.0.1-SNAPSHOT")

Endpoint — projections

Operation URL Response

GET

/actuator/projections

List of status objects for every registered projection.

GET

/actuator/projections/{name}

Status for one projection; 404 when the name is unknown.

GET

/actuator/projections/{name}/partitions

Per-partition breakdown (partition, owner, checkpoint, lag, generation, leaseExpiry, state, sources) for a partitioned projection; 404 when the name is unknown or the projection is not partitioned.

POST

/actuator/projections/{name}/{action}

Triggers a management operation; 400 on an unknown action or unknown name.

action must be one of replay, resume, or halt. For what each operation does — including the full ProjectionState lifecycle — see Delivery & error handling. replay on a @Partitioned projection rebuilds it in place, distributed across its bucket owners — see What an in-place partitioned rebuild resets. A partitioned projection with no pollable source cannot be rebuilt this way and is rejected with a reason in the response rather than accepted.

For a partitioned projection, /actuator/projections/{name} additionally carries partitioned: true and a partitionSummary object (partitions, owned, stuck, minCheckpoint, maxLag); its checkpoint and lag aggregate across the partitions (minimum checkpoint, maximum lag) rather than reflecting the unused single-owner cursor.

Each entry in a bucket’s sources array describes one input that bucket drains — sourceId, cursor, frontier, and lag — and is where the bucket’s own checkpoint and lag come from: the smallest cursor and the largest per-source lag across them. A bucket that drains both an event store and an upstream read-model feed measures those inputs in different units, so the per-source entries are the only place the two can be read like for like.

Health — projections

The component appears under /actuator/health/projections.

  • DOWN — if any projection is in the HALTED state.

  • DOWN — if any projection’s lag exceeds the spring.ddd.cqrs.projection.actuator.lag-threshold property (default: null, i.e. disabled; must be a positive value to activate lag-based degradation).

  • UP — all other states, including SUSPENDED and REPLAYING, are treated as healthy.

Per-projection detail in the health response includes state, lag, checkpoint, highWaterMark, and — whenever a halt reason is recorded, including the pause that precedes a rebuild — haltReason, haltedAt, and error. For a partitioned projection, the detail additionally includes stuckPartitions — the count of buckets with no live owner that still have work to do.

For the full list of configuration properties, see Configuration properties.

Metrics

Per-projection gauges are snapshotted from the set of registered projections at startup; values are read live on every scrape.

Meter Type Tags Meaning

cqrs.projection.checkpoint

Gauge

projection (e.g. account-summary)

Last committed event position for the projection.

cqrs.projection.lag

Gauge

projection

Events behind the global high-water mark.

cqrs.projection.generation

Gauge

projection

Replay generation counter.

cqrs.projection.partitions.owned

Gauge

projection

Number of partitions currently held under a live lease (partitioned projections only).

cqrs.projection.partitions.stuck

Gauge

projection

Number of partitions with no live owner that still have work to do (partitioned projections only) — the stuck-bucket signal.

cqrs.projection.halted

Gauge

projection

1 when the projection is HALTED, 0 otherwise.

cqrs.projection.suspended

Gauge

projection

1 when the projection is SUSPENDED, 0 otherwise.

cqrs.projection.hwm

Gauge

Global event-store high-water mark seen by the projection consumer.

cqrs.projection.hwm.blocked.seconds

Gauge

Seconds the high-water mark has been held at an unresolved position gap (0 when at rest). Normally near zero; a sustained value means a long-running database transaction is holding the frontier back — projections lag but never lose data, and catch up once it ends. Watch this if projections appear stalled.

cqrs.projection.halted.count

Gauge

Total number of projections currently in the HALTED state.

cqrs.projection.inmemory.size

Gauge

projection

Read-model entry count for each in-memory projection; only emitted when in-memory projections are present.

For a partitioned projection, cqrs.projection.checkpoint and cqrs.projection.lag report the aggregate (minimum checkpoint, maximum lag) across its partitions.

Partition observability

A partitioned projection — for example, an account-summary projection sharded by account ID — distributes its buckets across the running instances, and each bucket settles into one of three states:

  • OWNED — a live lease holds the partition; one instance is actively applying events for the accounts in that bucket.

  • ORPHANED — no instance currently holds a live lease, and the bucket is still behind the high-water mark. This is the actionable stuck signal: work is waiting and nobody owns it. It is expected to be transient during a rebalance (an instance restarting or a lease handing over) but should clear on its own within one lease cycle; a bucket that stays ORPHANED is worth investigating.

  • IDLE — no instance currently holds a live lease, but the bucket has fully caught up. This is normal and requires no action.

Each partition also carries a generation counter that increments every time the partition is claimed. A generation that climbs steadily over time — rather than staying flat — indicates claim churn: the bucket is repeatedly changing hands between instances, which is worth investigating even if the bucket is not currently ORPHANED.

Sagas

Add spring-ddd-starter-saga-actuator:

implementation("de.dwittkoetter:spring-ddd-starter-saga-actuator:0.0.1-SNAPSHOT")

Endpoint — sagas

The endpoint is read-only.

Operation URL Response

GET

/actuator/sagas

{ sagas: […​] } — status for every saga type known to the store.

GET

/actuator/sagas/{type}

{ type, active, completed, pendingDeadlines? }pendingDeadlines is absent (null) when spring.ddd.saga.deadline.enabled=false; 404 when the type has no instances.

Health — sagaStore

The component appears under /actuator/health/sagaStore.

  • UP — includes a sagaTypes detail with the number of known saga types.

  • DOWN — if the underlying query throws an exception (for example, the schema is not yet provisioned), the exception detail is included.

Metrics

Per-type gauges are snapshotted from the set of known saga types at startup; values are read live on every scrape.

Meter Type Tags Meaning

saga.instances

Gauge

type (e.g. transfer-saga), state=active|completed

Number of saga instances per type and lifecycle state.

saga.deadlines.pending

Gauge

type

Number of pending deadline callbacks per saga type; only emitted when spring.ddd.saga.deadline.enabled=true.