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 |
|---|---|---|
|
|
|
|
|
|
Health — eventStore
The component appears under /actuator/health/eventStore.
-
UP — includes a
positiondetail 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 |
|---|---|---|---|
|
Gauge |
— |
Global maximum event-store position (high-water mark). |
|
Counter |
|
Number of events appended per aggregate type. |
|
Timer |
|
Duration of each |
|
Counter |
|
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 |
|---|---|---|
|
|
List of status objects for every registered projection. |
|
|
Status for one projection; |
|
|
Per-partition breakdown ( |
|
|
Triggers a management operation; |
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
HALTEDstate. -
DOWN — if any projection’s
lagexceeds thespring.ddd.cqrs.projection.actuator.lag-thresholdproperty (default:null, i.e. disabled; must be a positive value to activate lag-based degradation). -
UP — all other states, including
SUSPENDEDandREPLAYING, 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 |
|---|---|---|---|
|
Gauge |
|
Last committed event position for the projection. |
|
Gauge |
|
Events behind the global high-water mark. |
|
Gauge |
|
Replay generation counter. |
|
Gauge |
|
Number of partitions currently held under a live lease (partitioned projections only). |
|
Gauge |
|
Number of partitions with no live owner that still have work to do (partitioned projections only) — the stuck-bucket signal. |
|
Gauge |
|
|
|
Gauge |
|
|
|
Gauge |
— |
Global event-store high-water mark seen by the projection consumer. |
|
Gauge |
— |
Seconds the high-water mark has been held at an unresolved position gap ( |
|
Gauge |
— |
Total number of projections currently in the |
|
Gauge |
|
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 staysORPHANEDis 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 |
|---|---|---|
|
|
|
|
|
|
Health — sagaStore
The component appears under /actuator/health/sagaStore.
-
UP — includes a
sagaTypesdetail 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 |
|---|---|---|---|
|
Gauge |
|
Number of saga instances per type and lifecycle state. |
|
Gauge |
|
Number of pending deadline callbacks per saga type; only emitted when |