When Spring Event Is the Wrong Abstraction
Spring events reduce in-process coupling, but they do not provide durable delivery or atomic rollback. Use these decision rules before shipping them.
The right way to evaluate Spring Event is to start with delivery guarantees, not convenience. If a notification is local to one application and can be retried after the main operation succeeds, Spring events are a clean option. If the event must be durable, cross service boundaries, or participate in a reversible transaction, a different mechanism is required.
What the abstraction actually does
An ApplicationEventPublisher sends an event into the Spring application context. Spring locates compatible listener methods, including methods annotated with @EventListener. This removes direct references between the publisher and its in-process subscribers. It does not automatically add a durable log, a consumer offset, a dead-letter queue, a replay API, or a cross-instance delivery contract.
That boundary is easy to miss because the happy path looks like a message bus. The execution model is different. A process restart can erase an event that was only in memory, and a context in the middle of destruction cannot safely resolve new beans for a late publication.
Shutdown must close the traffic window first
A rolling deployment should stop accepting new work before it closes the context. Deregister the instance, drain HTTP and RPC requests, pause message consumption, stop scheduled work, and wait within a defined deadline. Closing the ApplicationContext while late requests can still call publishEvent creates a race between business code and bean destruction.
The same discipline applies to startup. Do not treat an initialized process as a ready service. Register consumers and open traffic only after the context is refreshed and the dependencies used by listeners are healthy. Early consumer startup is a common way to turn a listener-registration race into silent message loss.
Separate business atomicity from notification
A checkout flow that needs inventory, payment, and order state to succeed or fail together should be expressed as an explicit application workflow. A collection of independent listeners cannot, by itself, tell the publisher which side effects completed or provide a simple rollback protocol. For cross-service operations, use a transaction boundary plus an outbox, a durable broker, or a compensation state machine.
A confirmed fulfillment event is different. Updating a settlement system, search index, reporting store, or cache after fulfillment can tolerate a delay if the subscriber retries and the failure becomes visible. That is where an in-process event can reduce coupling without pretending that the notification is part of the original atomic transaction.
Design subscribers for at-least-once execution
Even if the current implementation appears to call a subscriber once, retries, operator replays, and deployment behavior make duplicate execution a normal possibility. Store a stable event ID, make state transitions conditional, enforce uniqueness in the database, and pass an idempotency key to external services. Keep retry policies bounded and classify errors: a timeout may be retriable, while invalid data usually needs correction rather than another immediate attempt.
Synchronous versus asynchronous listeners
With synchronous listeners, an exception may be visible to the publisher through publishEvent. That can be useful when the notification is part of the caller’s success criteria, but it also couples latency and failure. With @Async, the publisher returns before the subscriber finishes; the executor, queue, metrics, rejection policy, and error handler become the real delivery boundary. Neither mode provides durability by itself.
Decision rules for a code review
- Use Spring Event for independent in-process reactions with a defined loss and retry policy.
- Use a broker or outbox when delivery must survive restart, cross a service boundary, or be replayed.
- Keep strong-consistency steps in an explicit transaction or workflow, not in unrelated listeners.
- Block traffic before context destruction and open traffic after readiness, not merely process start.
- Test duplicate delivery, partial subscriber failure, early startup messages, and late shutdown requests.
The useful question is not whether Spring Event is elegant. It is whether its delivery and failure semantics match the business promise. Once that is written down, the choice between an application event, a broker, and a transaction workflow becomes straightforward.