Master Spring Boot 3 Event Handling: From @EventListener to Transactional Events

This article explains how to use Spring Boot's event mechanism—including @EventListener, @TransactionalEventListener, and asynchronous processing—by walking through practical code examples, configuration tips, and common pitfalls to help developers build loosely‑coupled, reliable services.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Master Spring Boot 3 Event Handling: From @EventListener to Transactional Events

Environment

Spring Boot 3.4.2

1. Introduction

In Spring Boot development, using the event mechanism to achieve component decoupling is a powerful technique. Traditional tightly‑coupled code makes modules interdependent, so a change in one place can affect the whole system. Spring Boot’s event mechanism, based on the observer pattern and the @EventListener annotation, lets publishers emit events without knowing the listeners, and listeners handle specific events, greatly reducing module dependencies and improving maintainability and extensibility.

This article details the configuration and best practices of using @EventListener in Spring Boot.

2. Practical Cases

2.1 Create an Event Object

public class CreditScoreChangedEvent {
  private final String customerId;
  private final int oldScore;
  private final int newScore;

  public CreditScoreChangedEvent(String customerId, int oldScore, int newScore) {
    this.customerId = customerId;
    this.oldScore = oldScore;
    this.newScore = newScore;
  }
  // getters
}

Since Spring 4.2, event objects no longer need to extend ApplicationEvent . Spring will automatically wrap non‑ApplicationEvent objects into PayloadApplicationEvent .

2.2 Publish an Event

@Service
public class CreditService {
  private final ApplicationEventPublisher eventPublisher;
  private final ApplicationContext context;

  public CreditService(ApplicationEventPublisher eventPublisher, ApplicationContext context) {
    this.eventPublisher = eventPublisher;
    this.context = context;
  }

  public void scoreEvent() {
    // this.eventPublisher.publishEvent(new CreditScoreChangedEvent("p001", 100, 200));
    this.context.publishEvent(new CreditScoreChangedEvent("p001", 100, 200));
  }
}

Both ApplicationEventPublisher and ApplicationContext can be used to publish events; the latter inherits from the former.

2.3 Event Listener

@Component
public class CreditListener {
  @Async
  @EventListener
  public void scoreListener(CreditScoreChangedEvent event) {
    System.err.printf("%s - Received event: %s%n", Thread.currentThread().getName(), event);
  }
}

Enable asynchronous support:

@Configuration
@EnableAsync
public class AppConfig {
}

Console output example:

task-1 - Received event: com.pack.events.CreditScoreChangedEvent@74f2d4b3

2.4 Asynchronous Events & Transactions

If an event is published before the surrounding transaction commits, the event may be consumed even if the transaction later rolls back, causing data inconsistency. Use @TransactionalEventListener to ensure the event is only published after a successful transaction commit.

// UserService
@Transactional
public void save(User user) {
  this.jdbcClient.sql("insert into t_user (name, age, email) values (?, ?, ?)")
      .param(1, user.getName())
      .param(2, user.getAge())
      .param(3, user.getEmail())
      .update();
  this.context.publishEvent(new UserEvent(user.getName()));
}

// BusinessService
@Transactional
public void create(User user) {
  this.userService.save(user);
  // simulate error
  System.err.println(1 / 0);
}

// Event Listener
@Async
@EventListener
public void createUserEvent(UserEvent event) {
  System.err.printf("%s - Created user [%s]%n", Thread.currentThread().getName(), event.getName());
}

When the transaction rolls back, the listener still receives the event, which is undesirable. Replacing @EventListener with @TransactionalEventListener fixes the issue:

@Async
@TransactionalEventListener
public void createUserEvent(UserEvent event) {
  // ...
}

After the fix, the console shows that the event is only processed after a successful commit.

2.5 Global Asynchronous Event Handling

Annotating each listener with @Async can be cumbersome. A global asynchronous executor can be configured:

@Bean(name = AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME)
ApplicationEventMulticaster eventMulticaster(ConfigurableListableBeanFactory beanFactory) {
  SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster(beanFactory);
  ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
  taskExecutor.setCorePoolSize(10);
  taskExecutor.setMaxPoolSize(10);
  taskExecutor.setThreadNamePrefix("pack-event-");
  taskExecutor.afterPropertiesSet();
  multicaster.setTaskExecutor(taskExecutor);
  return multicaster;
}

Note: Starting from Spring 6.1, transactional listeners cannot run in asynchronous threads.

Testing

Two listeners were tested:

// 1. @TransactionalEventListener
// 2. @EventListener
public void createUserEvent(UserEvent event) {
  System.err.printf("%s - Created user [%s]%n", Thread.currentThread().getName(), event.getName());
}

Console screenshots (illustrated below) show the different behaviors.

These examples demonstrate how to correctly use Spring Boot’s event system, combine it with asynchronous execution, and avoid pitfalls when transactions are involved.

Javabackend developmentSpring BootAsyncEventListenertransactionaleventlistener
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.