Skip to main content
Spring Framework · Topic 5 of 25

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 (main in 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​

EventWhen
ContextRefreshedEventContext fully initialised (may fire more than once with hierarchies)
ContextStartedEvent / ContextClosedEventstart() / close()
ApplicationStartingEvent, ApplicationEnvironmentPreparedEventVery early (use spring.factories listeners, not beans)
ApplicationStartedEventContext refreshed, runners not yet called
ApplicationReadyEventRunners finished: application ready to serve (warm-up, registration with discovery)
AvailabilityChangeEventLiveness/readiness state changes (Kubernetes probes)
ApplicationFailedEventStartup 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 eventsMessage broker (Kafka, RabbitMQ)
ScopeOne JVMAcross services
Delivery guaranteeNone (in-memory)Durable, at-least-once
Failure handlingException propagates (sync) or is lost (async)Retry, dead-letter, replay
LatencyMethod-call speedNetwork
Use forDecoupling modules in a modular monolith, post-commit hooksIntegration 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 @EventListener for side effects (emails) that must only happen on commit.
  • @TransactionalEventListener without 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​

Quick quiz3 questions

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 ApplicationReadyEvent for post-startup work.
Next topicSpring Boot

Finished this page? Track your progress (saved only in your browser).