skip to content

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