Application Events
Definition
Application events let one part of the program announce that something happened, and other parts react through listeners. By default listeners run right away in the same thread, and you can make them asynchronous or run them only after a transaction commits.
Why it exists
Calling every interested class directly ties the sender to all of them, so each new reaction means editing the sender. Events reverse that: the sender announces, anyone can listen.
How it works
Synchronous by default: the crucial fact
publishEvent invokes listeners in the publisher's thread, one after another, before it returns. Consequences, all confirmed in a test:
- Listeners run on the same thread (
mainin my test, the request thread in a web app). - They run inside the publisher's transaction if there is one.
- A listener exception propagates to the publisher and can abort the business operation (and roll back its transaction).
- A slow listener slows the publisher.
That is usually what you want for in-process orchestration, and a trap if you expected fire-and-forget.
Transaction-bound events
Sending an email inside the transaction fires even if the transaction later rolls back, and a listener querying the new order may not see uncommitted data from another thread. Use @TransactionalEventListener:
@Component
class OrderEventHandler {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
void afterCommit(OrderPlaced event) { /* notify: only if the order is really committed */ }
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
void afterRollback(OrderPlaced event) { /* compensate */ }
}
Phases: BEFORE_COMMIT, AFTER_COMMIT (default), AFTER_ROLLBACK, AFTER_COMPLETION. If there is no active transaction when the event is published, the listener is not invoked at all unless fallbackExecution = true.
Careful with AFTER_COMMIT listeners that need to write to the database: the original transaction is committed but its resources may still be bound, so a plain @Transactional in the listener joins nothing and does not start a new transaction by default. Mark such a listener @Transactional(propagation = Propagation.REQUIRES_NEW) (Spring 6.1+ requires REQUIRES_NEW or NOT_SUPPORTED for @TransactionalEventListener methods that are also @Transactional).
AFTER_COMMIT is still not reliable delivery: if the process dies right after commit, the event is lost. For guaranteed publication to other services use the transactional outbox.
Built-in lifecycle events
| Event | When |
|---|---|
ContextRefreshedEvent | Context fully initialised (may fire more than once with hierarchies) |
ContextStartedEvent / ContextClosedEvent | start() / close() |
ApplicationStartingEvent, ApplicationEnvironmentPreparedEvent | Very early (use spring.factories listeners, not beans) |
ApplicationStartedEvent | Context refreshed, runners not yet called |
ApplicationReadyEvent | Runners finished: application ready to serve (warm-up, registration with discovery) |
AvailabilityChangeEvent | Liveness/readiness state changes (Kubernetes probes) |
ApplicationFailedEvent | Startup failed |
@EventListener(ApplicationReadyEvent.class)
void warmUp() { /* populate caches after startup without blocking context refresh */ }
Example code
Publishing and listening
record OrderPlaced(long orderId, String customerEmail) { } // events can be plain objects (Spring 4.2+)
@Service
class OrderService {
private final ApplicationEventPublisher events;
OrderService(ApplicationEventPublisher events) { this.events = events; }
@Transactional
public void place(Order order) {
// ... save ...
events.publishEvent(new OrderPlaced(order.id(), order.email()));
}
}
@Component
class EmailNotifier {
@EventListener
void on(OrderPlaced event) { /* send confirmation */ }
}
@Component
class LoyaltyService {
@EventListener
@Order(1) // ordering among listeners
void on(OrderPlaced event) { /* add points */ }
}
Listener methods can return a value, which is published as a new event (chaining), take generic events (EntityCreated<Customer> with resolvable generics), and filter with SpEL: @EventListener(condition = "#event.orderId() > 100").
Asynchronous listeners
@Configuration
@EnableAsync
class AsyncConfig { }
@Component
class AnalyticsListener {
@Async // runs on a TaskExecutor thread
@EventListener
void on(OrderPlaced event) { /* send to analytics */ }
}
Now the publisher does not wait and does not see listener exceptions (handle them with AsyncUncaughtExceptionHandler). The listener has no access to the publisher's transaction, security context or MDC unless you propagate them, and may run before the publisher's transaction commits (seeing no data!). Bound the executor (see Executors).
Domain events with Spring Data
Aggregate roots can collect domain events and let Spring Data publish them on save:
class Order extends AbstractAggregateRoot<Order> {
void confirm() {
this.status = Status.CONFIRMED;
registerEvent(new OrderConfirmed(id)); // published when the repository saves this aggregate
}
}
This keeps the domain model free of infrastructure while allowing other components to react.
System design considerations
- Make listeners idempotent and fast, or move slow work to an async executor
- Use the transactional outbox when events must reach other services
- Document which events exist, since they are a hidden coupling
When to use it, and when not
Use it when
- Several independent in-process reactions follow one action
- You trigger side effects only after commit (TransactionalEventListener)
- You publish domain events from aggregates
Think twice when
- The consumer lives in another service or must survive restarts (use a broker)
- You need guaranteed delivery or ordering
- A direct method call is clearer for a single consumer
Events vs messaging
| Spring events | Message broker (Kafka, RabbitMQ) | |
|---|---|---|
| Scope | One JVM | Across services |
| Delivery guarantee | None (in-memory) | Durable, at-least-once |
| Failure handling | Exception propagates (sync) or is lost (async) | Retry, dead-letter, replay |
| Latency | Method-call speed | Network |
| Use for | Decoupling modules in a modular monolith, post-commit hooks | Integration between services, durable workflows |
A common architecture: domain event inside the service → handler writes an outbox row in the same transaction → relay publishes to the broker.
Common mistakes
- Assuming async delivery and being surprised by exceptions and latency.
- Plain
@EventListenerfor side effects (emails) that must only happen on commit. @TransactionalEventListenerwithout an active transaction silently doing nothing.- Treating events as durable delivery.
- Large event graphs creating hidden control flow that is hard to trace; name events by business facts and keep the number of listeners understandable.
- Unbounded async executors.
Interview questions
1. Are Spring events synchronous?
Yes by default: listeners run sequentially in the publisher's thread, in its transaction, and their exceptions propagate. Use @Async or an ApplicationEventMulticaster with an executor for async delivery.
2. How do you make a listener run only after the transaction commits?
@TransactionalEventListener(phase = AFTER_COMMIT). It is skipped when no transaction is active (unless fallbackExecution = true).
3. How do you order listeners?
@Order on the listener methods or implementing Ordered; lower values run first.
4. Are Spring events a replacement for Kafka?
No: in-process, non-durable, no retries or replay. They suit intra-application decoupling, not cross-service integration.
5. What happens if a listener throws?
For synchronous listeners the exception propagates to publishEvent and may roll back the surrounding transaction; for async listeners it goes to the async exception handler.
6. Where do you put startup logic that must run after the application is ready?
ApplicationReadyEvent listener or an ApplicationRunner, rather than @PostConstruct which blocks context refresh.
7. Why might an async AFTER_COMMIT listener not find the data it needs?
@Async listeners combined with plain @EventListener may run before commit; use @TransactionalEventListener (after commit) to guarantee visibility.
Check yourself
1By default, a @EventListener runs...
2You want to send an email only after the database transaction commits. Which fits?
3When should you use a message broker instead of Spring events?
Key takeaways
- Events are synchronous in-process notifications by default, in the publisher's transaction and thread.
- Use
@TransactionalEventListener(AFTER_COMMIT)for effects that depend on a successful commit. - Make delivery durable through an outbox and a broker, not Spring events.
- Use
ApplicationReadyEventfor post-startup work.
Finished this page? Track your progress (saved only in your browser).