Spring Modulith Integration
Spring Modulith is an optional pairing, not a dependency.
Spring DDD publishes domain events through Spring’s ApplicationEventPublisher — JPA aggregates on repository save(); the event-sourced path after a successful append — and works perfectly without Modulith.
When Spring Modulith is on the classpath its Event Publication Registry takes over the delivery lifecycle. The registry records a publication row per transactional listener within the originating business transaction, then drives after-commit dispatch: each listener is invoked once the transaction commits, and any listener that has not completed cleanly stays in Modulith’s failure lane to be retried rather than lost. This turns plain at-most-once Spring events into durable at-least-once delivery — and it is entirely a Modulith feature; Spring DDD’s events flow through it transparently.
Spring DDD adds one thing on top: the spring-ddd-starter-domain-events-modulith-jdbc starter, which bridges the gap between Modulith’s publication log and the message metadata attached to each event at publish time, so that metadata survives post-restart resubmission.
At-least-once delivery
Modulith’s registry stores one row per (event × listener) pair in the same database transaction that published the event. After commit, each registered listener is invoked; a successful completion removes or stamps the row (depending on the configured retention). A listener that throws, or a process that crashes mid-dispatch, leaves the row incomplete; Modulith’s resubmitter picks it up on the next opportunity and retries.
Because delivery is at-least-once, handlers must be idempotent. See Handling events for handler patterns.
Both @DomainEventHandler (the framework-owned handler annotation) and @ApplicationModuleListener participate equally in the registry lifecycle.
The framework drives each @DomainEventHandler through ModulithDomainEventPublicationCompleter, which calls markProcessing, markCompleted, or markFailed on the EventPublicationRegistry using the listener’s registered identifier.
To resubmit publications that were incomplete when the process last stopped, enable Modulith’s restart-time resubmitter:
# Replay incomplete publications on application startup (Modulith property)
spring.modulith.events.republish-outstanding-events-on-restart=true
For high-throughput use cases, recent Spring Modulith versions also offer a transactional-outbox mode, which shifts the dispatch step out of the commit thread entirely:
spring.modulith.events.externalization.mode=outbox
See the Spring Modulith events reference for the full feature set.
Externalization
@Externalized — a Spring Modulith annotation — routes a domain event to a message broker (Kafka, RabbitMQ, or any supported transport) as an after-commit step driven by the same registry.
If the broker is unavailable the publication row stays incomplete for later resubmission; the broker outage does not roll back the business transaction.
Spring DDD events are plain objects, so attaching @Externalized to a domain-event class is all that is needed:
@Externalized("banking.account.money-deposited") (1)
@DomainEvent(namespace = "banking", name = "MoneyDeposited")
data class MoneyDeposited(
@AggregateId val accountId: AccountId,
val amount: Money,
)
| 1 | @Externalized is a Spring Modulith annotation; the routing key is the topic or exchange name used by the configured transport. |
Externalization is a Modulith feature. Spring DDD’s events flow through it without any framework-specific wiring.
Native propagation of message metadata as broker message headers via @Externalized is a future enhancement.
A custom EventExternalizer can read CurrentMessageMetadata.get() today — see the next section.
|
Durable domain-event metadata
The gap
Message metadata (tenant, actor, correlation id, and other values declared with @MetadataValue) is bound to the event instance at publish time and is accessible from any handler on the same thread.
For live @DomainEventHandler and @ApplicationModuleListener calls this works automatically.
The gap appears on resubmission.
When Modulith restarts and replays an incomplete publication, or retries a failed one, the original event instance is freshly deserialized from the publication store.
The metadata that was attached at publish time is gone — the resubmitted handler sees MessageMetadata.EMPTY.
The bridge
Add spring-ddd-starter-domain-events-modulith-jdbc alongside a Modulith event-store starter:
implementation("de.dwittkoetter:spring-ddd-starter-domain-events-modulith-jdbc:0.0.1-SNAPSHOT")
// A Modulith event-store starter is also required — for example:
implementation("org.springframework.modulith:spring-modulith-starter-jpa")
// or: implementation("org.springframework.modulith:spring-modulith-starter-jdbc")
The starter auto-configures when Modulith’s EventPublicationRegistry is on the classpath.
It decorates Modulith’s EventPublicationRepository with MetadataPersistingEventPublicationRepository, which:
-
on publish — persists the event’s bound
MessageMetadatato a side table (EVENT_PUBLICATION_METADATA) in the same transaction as the publication row; nothing is written for events with no metadata. -
on resubmit — before Modulith re-multicasts an incomplete or failed publication, the decorator re-binds the stored metadata to the freshly deserialized event instance so that
@MetadataValue("tenant-id")andCurrentMessageMetadata.get()return the original values.
No spring.ddd.* properties are introduced; all configuration uses standard Modulith properties.
A custom event externalizer can also read CurrentMessageMetadata.get() and see the publish-time metadata — on first publish and on every post-restart replay, because the side table rebinds before re-externalization runs.
Spring DDD restores the metadata scope around after-commit @EventListener-style listeners (including the @ApplicationModuleListener that drives Modulith externalization), so the value is reachable wherever externalization happens:
@Bean
fun metadataAwareExternalizer(
configuration: EventExternalizationConfiguration,
): DelegatingEventExternalizer =
DelegatingEventExternalizer(configuration) { target, payload -> (1)
val tenantId = CurrentMessageMetadata.get()["tenant-id"] (2)
// hand (target, payload, tenantId) to your transport — e.g. copy tenantId onto the outbound headers
CompletableFuture.completedFuture(null)
}
| 1 | A standard Spring Modulith externalizer bean; target is the routing target and payload is the event. |
| 2 | The publish-time metadata is in scope here, restored by Spring DDD around the after-commit listener — present on the first publish and on every post-restart resubmission. |
Retention
The side table mirrors Modulith’s own retention policy, controlled by spring.modulith.events.completion-mode:
| Mode | Effect on EVENT_PUBLICATION_METADATA |
|---|---|
|
The row is retained and its |
|
The row is deleted as soon as the publication completes. |
|
The row is deleted when the publication completes. Archiving to a separate audit table is a future enhancement; a startup warning is emitted when this mode is active. |
Schema management
The EVENT_PUBLICATION_METADATA table is created automatically when Modulith’s schema initialization is enabled:
# Opt-in (same switch that creates Modulith's own event publication table)
spring.modulith.events.jdbc.schema-initialization.enabled=true
The table is placed in the same schema prefix as Modulith’s own tables, controlled by spring.modulith.events.jdbc.schema.
Six dialects are supported: H2, HSQLDB, MariaDB, MySQL, PostgreSQL, and SQL Server.
For the exact CREATE TABLE statement for each dialect, see Database table schemas.
When auto-init is disabled (the default), the table must be created manually before the application starts. If the table is absent at startup a warning is logged — the application continues, but metadata will not be persisted and resubmitted handlers will see empty metadata.
Related pages
-
Handling events —
@DomainEventHandler, idempotency, and delivery semantics -
Message metadata — declaring and reading
@MetadataValueandCurrentMessageMetadata -
Sagas — at-least-once delivery for long-running process managers
-
Testing — testing event handlers and metadata in Spring DDD