After completing user registration, you want to send a welcome email. After an order is confirmed, you want to update the inventory service. If you handle these by calling MailService.sendWelcome() directly from inside UserService, you’ll soon find yourself with a spaghetti mess of dependencies between service classes.

But introducing Kafka just for this feels like overkill. That’s where Spring’s ApplicationEvent comes in. It’s a simple in-JVM event bus that lets you decouple service dependencies in just a few dozen lines of code.

What is ApplicationEvent?

It’s Spring’s built-in event bus that operates within the JVM. The publisher simply fires an event saying “this thing happened,” and the listener receives and handles it. The key point is that neither side needs to know about the other directly.

The decision between ApplicationEvent and an external broker like Kafka is straightforward: if everything happens within the same JVM process, use ApplicationEvent; if you need to notify separate processes, use an external broker.

Creating a Custom Event Class

Since Spring 4.2, you can use plain POJOs as events — no need to extend ApplicationEvent.

public class UserRegisteredEvent {
    private final Long userId;
    private final String email;

    public UserRegisteredEvent(Long userId, String email) {
        this.userId = userId;
        this.email = email;
    }

    public Long getUserId() { return userId; }
    public String getEmail() { return email; }
}

Embed the data you need at that point in the event class, and your listeners won’t have to make redundant database calls.

Publishing Events with ApplicationEventPublisher

Just inject ApplicationEventPublisher and call publishEvent().

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public void register(String email) {
        User user = userRepository.save(new User(email));
        eventPublisher.publishEvent(new UserRegisteredEvent(user.getId(), email));
    }
}

The publisher has no idea who is listening to the event. You can add or remove listeners without touching UserService at all — that’s the benefit of loose coupling.

Subscribing with @EventListener

Simply annotate a Bean method with @EventListener to make it a listener.

@Component
@RequiredArgsConstructor
public class WelcomeMailListener {

    private final MailService mailService;

    @EventListener
    public void onUserRegistered(UserRegisteredEvent event) {
        mailService.sendWelcome(event.getEmail());
    }
}

The method parameter type determines which event it receives. To listen for multiple types, use @EventListener(classes = {EventA.class, EventB.class}).

Processing After Commit with @TransactionalEventListener

There’s a pitfall in the code above. If you publish an event inside a @Transactional method and the transaction is rolled back, the listener will still execute. You don’t want an email going out when user registration actually failed.

@TransactionalEventListener lets you tie listener execution to a specific transaction phase.

@Component
@RequiredArgsConstructor
public class WelcomeMailListener {

    private final MailService mailService;

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onUserRegistered(UserRegisteredEvent event) {
        mailService.sendWelcome(event.getEmail());
    }
}

AFTER_COMMIT (the default) runs only after a successful commit. AFTER_ROLLBACK runs after a rollback, and AFTER_COMPLETION runs after either a commit or rollback.

One important caveat: in the AFTER_COMMIT phase, the transaction has already ended. If you need to write to the database inside the listener, you must explicitly start a new transaction with @Transactional(propagation = Propagation.REQUIRES_NEW). See also Spring Boot Transaction Management for more details.

Asynchronous Handling with @Async

You probably don’t want time-consuming operations like sending emails to block the main thread. Combine @Async to handle them asynchronously.

First, enable @EnableAsync.

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.initialize();
        return executor;
    }
}

Then just add @Async to your listener.

@Async
@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
    mailService.sendWelcome(event.getEmail());
}

When combining @TransactionalEventListener with @Async, note that the transaction context is not propagated to the async thread. If you need a transaction inside the listener, start a new one with @Transactional(propagation = Propagation.REQUIRES_NEW).

For more on async processing, see How to Implement Async Processing in Spring Boot.

Controlling Execution Order of Multiple Listeners with @Order

If multiple listeners handle the same event, use @Order to specify execution order. Lower values run first.

@EventListener
@Order(1)
public void auditLog(UserRegisteredEvent event) { ... }

@EventListener
@Order(2)
public void sendNotification(UserRegisteredEvent event) { ... }

That said, introducing ordering dependencies between listeners undermines the loose-coupling benefits. The ideal design keeps each listener independently operable.

Testing with ApplicationEvents

Since Spring 5.3, you can easily verify event publishing using ApplicationEvents.

@SpringBootTest
@RecordApplicationEvents
class UserServiceTest {

    @Autowired
    private UserService userService;

    @Autowired
    private ApplicationEvents applicationEvents;

    @Test
    void shouldPublishUserRegisteredEventOnRegistration() {
        userService.register("[email protected]");

        assertThat(
            applicationEvents.stream(UserRegisteredEvent.class).count()
        ).isEqualTo(1);
    }
}

Annotating with @RecordApplicationEvents records all events published within the context, which you can then assert against using ApplicationEvents. The ability to test the publisher and listener independently is another advantage of ApplicationEvent.

Summary

ApplicationEvent is a great fit for situations where you want to clean up inter-service dependencies without the overhead of introducing Kafka. You can start with basic subscriptions using @EventListener, add transaction-safe handling with @TransactionalEventListener, and layer in async behavior with @Async — incrementally, as needed.

It’s not the right tool for notifying separate processes or delivering messages reliably across system restarts. For those requirements, refer to Implementing Kafka Producers and Consumers in Spring Boot.