What is the Spring Modulith Event Publication Registry, what problem does it solve, and how does it work?
answer
- transactional outbox for app events
- EVENT_PUBLICATION row written in publisher's transaction
- incomplete until listener completes
- completion-mode: UPDATE / DELETE / ARCHIVE
- republish-on-restart + IncompleteEventPublications = at-least-once
basics
~20 sBecause async after-commit listeners run outside the publisher's transaction, a crash could lose the event. The registry persists a publication row (in the publisher's transaction) for each listener, marks it complete when the listener finishes, and leaves failed ones incomplete for replay — giving at-least-once delivery.
solid answer
~40 sThe Event Publication Registry makes in-process events durable. Without it, an @ApplicationModuleListener runs async after commit, so a JVM crash or listener failure between commit and completion silently loses the event. With a starter like spring-modulith-starter-jpa, for every transactional listener Spring writes an EVENT_PUBLICATION row — the serialized event plus the listener id — as part of the original transaction, so it's durable exactly when the business data is. After the listener completes successfully, that publication is marked completed (updated, deleted, or archived depending on completion-mode). If the listener throws or the app dies, the row stays incomplete. Those incompletes can be replayed: automatically on restart with republish-outstanding-events-on-restart=true, or programmatically via the IncompleteEventPublications bean. Net effect is at-least-once delivery, which is why listeners must be idempotent. It's a transactional-outbox pattern implemented for application events.
go deeper
Know that a registry exists so events aren't lost on crash.
Explain the incomplete→complete lifecycle and that it needs a persistence starter.
Articulate it as a transactional outbox, describe the table, completion modes, and at-least-once/idempotency implications.
Weigh completion-mode and retention for table growth, plan event schema evolution for stored payloads, and treat the registry as the reliability substrate under externalization.
## The loss window it closes An `@ApplicationModuleListener` runs **async, after the publisher commits, in a new transaction**. That creates a gap: the order is committed, but the inventory listener hasn't run yet. If the JVM crashes in that gap, or the listener throws, the event is gone with no trace. In-memory Spring events give **no durability guarantee**. ## What the registry is The **Event Publication Registry** is a Spring Modulith component that persists a record of each event publication so it can be tracked to completion and, if necessary, replayed. It's the classic **transactional outbox** pattern applied to application events. You activate it by adding a persistence starter, e.g.: - `spring-modulith-starter-jpa` - `spring-modulith-starter-jdbc` - `spring-modulith-starter-mongodb` - (Neo4j, etc.) ## How it works, step by step 1. **On publish, before/at commit:** For **each** transactional event listener that will handle the event, the registry creates an **incomplete publication** entry and stores it in the same transaction as the business change. Default JDBC/JPA table `EVENT_PUBLICATION` holds columns like `ID`, `LISTENER_ID` (the target listener's method identity), `EVENT_TYPE`, `SERIALIZED_EVENT` (JSON, via a configured serializer such as Jackson), `PUBLICATION_DATE`, and `COMPLETION_DATE`. - Because the row is written **inside the publisher's transaction**, durability of the intent-to-notify is atomic with the business data. If the business transaction rolls back, the publication row rolls back too — no ghost events. 2. **After commit:** the async listener runs (in its REQUIRES_NEW transaction). 3. **On success:** the registry **completes** the publication. What 'complete' means depends on `spring.modulith.events.completion-mode`: - `UPDATE` (default): sets `COMPLETION_DATE` on the row. - `DELETE`: removes the row entirely (keeps the table small). - `ARCHIVE`: moves it to an archive table. 4. **On failure / crash:** the publication stays **incomplete** (no completion date, still present). It's now recoverable. ## Recovery of incomplete publications - **On restart:** set `spring.modulith.events.republish-outstanding-events-on-restart=true` (default is false) and, at startup, Spring resubmits all still-incomplete publications to their listeners. - **Programmatically / scheduled:** inject the `IncompleteEventPublications` bean and call `resubmitIncompletePublicationsOlderThan(Duration)` or `resubmitIncompletePublications(Predicate)` — e.g. from a `@Scheduled` job to retry things stuck for more than a few minutes. - **Housekeeping:** `CompletedEventPublications` lets you purge old completed rows (`deletePublicationsOlderThan(Duration)`) if you keep them (UPDATE mode). ## Key consequences a senior must state - **Delivery is at-least-once, not exactly-once.** A listener can run again on replay (or the app may crash *after* the listener's side effect but *before* the completion is written). Handlers must be **idempotent**. - **Per-listener granularity:** two listeners for the same event get two independent publication rows; one can complete while the other stays incomplete and is retried alone. - **Serialization matters:** the event is serialized (default JSON). Events should be serializable value objects (records), and you must consider schema evolution for events sitting in the table across a deploy. - **It's opt-in via a starter.** No persistence starter = plain in-memory events = no durability. ## When to rely on it Use it whenever a lost cross-module side effect would be a correctness problem (money, inventory, provisioning, outbound notifications). It's also the foundation that makes `@Externalized` broker publishing reliable, because externalized events are tracked the same way.
- Why is the publication row written inside the publisher's transaction rather than after commit?So durability of the notification intent is atomic with the business data. If the business transaction rolls back, the publication row rolls back with it (no ghost events); if it commits, the intent to notify is guaranteed persisted, closing the crash window. That's the transactional-outbox guarantee.
- The registry gives at-least-once. How do you avoid double side effects on replay?Make listeners idempotent: use natural/idempotency keys and upserts, dedupe on an already-processed marker, or make the operation naturally repeatable. Never assume exactly-once, because a crash between the side effect and writing the completion causes a legitimate replay.
- How do you activate the registry?Add a persistence starter (e.g. spring-modulith-starter-jpa or -jdbc), provide the EVENT_PUBLICATION schema, and configure completion-mode / republish-on-restart as desired. Without a starter you get plain in-memory events with no durability.
saying these in an interview costs you the question
- Claiming Spring events are durable by default without any starter
- Saying it provides exactly-once delivery
- Thinking the publication row is written after commit (it's inside the transaction)
- Forgetting handlers must be idempotent because of replay