Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Spring synchronous event listener failure: publication can fail the caller

Last updated: 5 Oct 20264 min read
tutorial
IntermediateBy AITrove Editorial

Default application event delivery runs listeners in the publisher's thread, so listener latency and exceptions become part of the caller's result.

Publication is usually a method call

A receipt service publishes ReceiptAccepted while saving a row. With the default multicaster, listeners run synchronously on that thread. A listener that throws can stop publication and propagate its exception to the service; if the service is still inside a transaction, that may roll back the save. An event name does not make the work asynchronous or durable. Application events are local in-process communication, and transaction phase listeners move selected work to a different commit point.

Define the failure owner

A required invariant, such as updating a same-database receipt index, can legitimately fail the command. A best-effort notification should not decide whether the receipt is accepted. Move the latter after commit and give it a retryable durable channel if losing it matters. Configuring a multicaster ErrorHandler to swallow every listener failure can make a broken required listener invisible; assign failure handling per use case rather than globally masking exceptions.

Exercise rollback and ordering

Register a listener that records a call, then one that throws. Publish inside a transaction and assert the command's database state matches the intended failure contract. Do not depend on an incidental listener iteration order; if order matters, declare it and test it, or call a service directly when the steps form one business operation. Measure listener duration because synchronous listeners extend request latency and hold the publisher's resources.

Implementation contract

Java
@Component
final class ReceiptIndexListener {
    private final ReceiptIndex receiptIndex;

    ReceiptIndexListener(ReceiptIndex receiptIndex) {
        this.receiptIndex = receiptIndex;
    }

    @EventListener
    void on(ReceiptAccepted accepted) {
        receiptIndex.record(accepted.receiptId());
    }
}

Cost and verification

Each synchronous listener adds its execution time to publishEvent and may hold the caller's transaction open. A slow remote call inside a listener can extend lock time and cause retries elsewhere.

Common Mistakes

  • Do not assume @EventListener makes delivery asynchronous or durable.
  • Do not swallow every listener error when some listeners enforce required state.
  • Do not call a remote service inside a transaction-scoped listener without a clear failure budget.

Read next

Spring application events: local delivery is not a durable queue, Spring transaction events: run a listener after commit without claiming durability, Spring async event listener: handle detached failures and queue pressure, Spring transactional outbox: commit a receipt and event row together, Spring transactional event test: cross an actual commit boundary.

spring
spring-boot
spring-core
application-event-listener-failure
Storage details