skip to content

Externalized & Published Events

@ApplicationModuleListener makes cross-module communication async and transactional, and the event publication registry records deliveries so incomplete ones can be retried. Interviewers ask how you avoid losing an event when the consumer fails, and the registry is the answer.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

5

Why do Spring Modulith applications communicate between modules with published events, and what does @ApplicationModuleListener do?

level: juniorimportance: must knowfreq 45%

answer

  1. publish event, not call the other module
  2. @ApplicationModuleListener = @TransactionalEventListener + @Async + REQUIRES_NEW
  3. runs after commit, on another thread, own transaction
  4. consumer stays out of publisher's allowedDependencies
  5. eventual consistency + at-least-once via registry

basics

~10 s

Instead of one module directly calling another, it publishes an event. A method marked @ApplicationModuleListener in the other module receives that event asynchronously after the publisher's transaction commits, so the modules stay loosely coupled.

solid answer

~40 s

Spring Modulith enforces module boundaries, and direct method calls create tight coupling. Instead, a module publishes a domain event through ApplicationEventPublisher, and another module consumes it with a method annotated @ApplicationModuleListener. That annotation is a composite that combines @TransactionalEventListener (runs after the publisher commits), @Async (runs on a separate thread), and @Transactional(propagation = REQUIRES_NEW) (its own transaction). So the consuming module never appears in the publisher's allowedDependencies — it only needs to know the event type. This gives loose coupling, independent testability, and lets you later route the same event to an external broker without touching publishers. The trade-off is eventual consistency and the need to handle listener failures, which Spring Modulith's Event Publication Registry addresses.

go deeper

for a junior

Know that modules publish events instead of calling each other, and that @ApplicationModuleListener consumes them after commit on another thread.

for a middle

Be able to name the three composed annotations and explain after-commit / async / new-transaction semantics.

for a senior

Discuss the eventual-consistency and message-loss trade-offs and how the Event Publication Registry closes the loss window.

for a principal

Frame events as the seam that lets a modular monolith later split into services, with externalization replacing in-process delivery.

## The coupling problem Spring Modulith organizes a monolith into **application modules** (top-level packages) and enforces that each module only depends on the modules listed in its allowed dependencies (verified by `ApplicationModules.verify()` / ArchUnit). If the `order` module calls a bean in the `inventory` module directly, `order` now *depends on* `inventory` — its code, its transactions, its failures. That's exactly the coupling modularization is meant to avoid. ## Events as the decoupling mechanism Instead, the publishing module emits a **domain event** — a plain object (often a Java `record`) describing something that happened: ```java publisher.publishEvent(new OrderCompleted(order.getId())); ``` `publisher` is Spring's `ApplicationEventPublisher`. The consuming module registers a listener for that event *type*. Now the publisher knows nothing about who consumes it — it only knows the event class (which typically lives in the publisher's own package or a shared API package). You can add, remove, or replace consumers without recompiling the publisher. ## What `@ApplicationModuleListener` is `@ApplicationModuleListener` is a **meta-annotation** (a shortcut that stacks three annotations) provided by `org.springframework.modulith.events`: - **`@TransactionalEventListener`** — the listener only fires *after the publishing transaction commits* (default phase `AFTER_COMMIT`). If the publisher rolls back, the listener never runs. - **`@Async`** — the listener runs on a **different thread** than the publisher, so it doesn't block the publishing request. - **`@Transactional(propagation = REQUIRES_NEW)`** — the listener runs in **its own fresh transaction**, independent of the (already-committed) publisher transaction. So the typical consumer looks like: ```java @ApplicationModuleListener void on(OrderCompleted event) { /* update inventory */ } ``` ## Why this shape matters Running *after commit*, *async*, in a *new transaction* is what makes modules truly independent: the inventory update succeeding or failing no longer affects whether the order is saved. But it also introduces two realities a junior should name: 1. **Eventual consistency** — the inventory change happens slightly after the order is saved, not atomically. 2. **Possible message loss** — because the listener runs after commit on another thread, a crash in that window would drop the event. Spring Modulith's **Event Publication Registry** exists precisely to make delivery durable (at-least-once). ## When to use it Use published events for cross-module side effects that can tolerate eventual consistency (notifications, projections, downstream updates). Use a direct synchronous API call when you need an immediate answer *inside* the same request/transaction.

  • If the publishing transaction rolls back, does the @ApplicationModuleListener still fire?
    No. It uses @TransactionalEventListener with the default AFTER_COMMIT phase, so it only runs once the publisher's transaction commits successfully. A rollback means the listener is never invoked.
  • When would you prefer a direct *Api call over an event?
    When you need a synchronous result or strong consistency within the same request/transaction — e.g. reading data or validating something before proceeding. Events fit fire-and-forget side effects that tolerate eventual consistency.

saying these in an interview costs you the question

  • Thinking the listener runs inside the publisher's transaction (it runs after commit, in a new one)
  • Claiming events make the two modules mutually dependent (only the event type is shared)
  • Assuming delivery is synchronous and immediate

context

open as a page

What is the Spring Modulith Event Publication Registry, what problem does it solve, and how does it work?

level: seniorimportance: must knowfreq 38%

basics

~20 s

Because 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.

open as a page

Break down the semantics of @ApplicationModuleListener — what does each composed part mean, and what gotchas follow from async + AFTER_COMMIT + REQUIRES_NEW?

level: middleimportance: should knowfreq 40%

basics

~10 s

It stacks @TransactionalEventListener (fire after the publisher commits), @Async (run on another thread), and @Transactional(REQUIRES_NEW) (run in its own transaction). Gotchas: needs @EnableAsync, exceptions don't roll back the publisher, and delivery is eventually consistent.

open as a page

How does incomplete-event recovery work in Spring Modulith, and how would you operate it in production (restart replay, scheduled resubmit, cleanup, idempotency)?

level: seniorimportance: should knowfreq 26%

basics

~10 s

Failed listeners leave incomplete publication rows. Recover them by enabling republish-outstanding-events-on-restart, or by scheduling a job that calls IncompleteEventPublications.resubmitIncompletePublicationsOlderThan(...). Purge completed rows with CompletedEventPublications. Because resubmits re-run listeners, handlers must be idempotent.

open as a page

What does @Externalized do in Spring Modulith, and how does externalizing events to a broker relate to the Event Publication Registry?

level: principalimportance: should knowfreq 20%

basics

~20 s

@Externalized marks an application event to also be published to an external broker (Kafka, RabbitMQ, etc.). Its value 'target::key' sets the destination and a SpEL routing key. The same Event Publication Registry tracks externalization, giving durable at-least-once delivery to the broker.

open as a page