Skip to main content

Week 8 Day 2 — Spring Events

Goal

Today I want to understand Spring application events.

Main questions:

  1. What is a Spring event?
  2. Why use events?
  3. What is ApplicationEventPublisher?
  4. What is @EventListener?
  5. Are Spring events synchronous or asynchronous?
  6. What is a transactional event?
  7. What is @TransactionalEventListener?
  8. What does AFTER_COMMIT mean?
  9. When should I use events?
  10. When should I not use events?
  11. What are common exam traps?

1. Quick Review from Week 8 Day 1

In Day 1, I learned:

  • AOP means Aspect-Oriented Programming.
  • AOP separates cross-cutting concerns from business logic.
  • Spring AOP is proxy-based.
  • AOP works when calls go through the Spring proxy.
  • Self-invocation bypasses the proxy.
  • @Transactional, method security, caching, and async often rely on proxies.
  • Use AOP for technical cross-cutting concerns.

Memory sentence:

Spring AOP works by calling methods through a proxy.

Today I learn another Spring communication mechanism:

Spring application events.

2. What Is a Spring Event?

A Spring event is a message published inside the Spring application context.

Simple idea:

Something happened.
Other parts of the application may react.

Examples:

UserRegisteredEvent
TaskCreatedEvent
InvoicePaidEvent
DocumentUploadedEvent
PasswordResetRequestedEvent
ClientOnboardedEvent

Instead of one service directly calling many other services, it can publish an event.

Memory sentence:

A Spring event says: something happened inside the application.


3. Why Use Events?

Without events:

public void registerUser(RegisterUserRequest request) {
User user = userRepository.save(...);

emailService.sendWelcomeEmail(user);
auditService.auditUserRegistered(user);
crmService.syncUser(user);
notificationService.notifyAdmin(user);
}

Problem:

UserService knows too many other services.
The method becomes long.
Adding a new reaction requires changing UserService.
Business action and side effects are mixed together.

With events:

public void registerUser(RegisterUserRequest request) {
User user = userRepository.save(...);

eventPublisher.publishEvent(new UserRegisteredEvent(user.getId(), user.getEmail()));
}

Then listeners react:

WelcomeEmailListener
AuditListener
CrmSyncListener
AdminNotificationListener

Memory sentence:

Events reduce direct coupling between the main action and side effects.


4. Direct Call vs Event

Direct call

UserService -> EmailService
UserService -> AuditService
UserService -> NotificationService

Event style

UserService -> publishes UserRegisteredEvent

WelcomeEmailListener listens
AuditListener listens
NotificationListener listens

Important:

The publisher does not need to know all listeners.

Memory sentence:

The publisher knows the event, not every reaction.


5. Event Publisher

Spring provides:

ApplicationEventPublisher

It is used to publish events.

Example:

@Service
public class UserRegistrationService {

private final UserRepository userRepository;
private final ApplicationEventPublisher eventPublisher;

public UserRegistrationService(
UserRepository userRepository,
ApplicationEventPublisher eventPublisher
) {
this.userRepository = userRepository;
this.eventPublisher = eventPublisher;
}

@Transactional
public UserDto register(RegisterUserRequest request) {
UserEntity user = new UserEntity(request.email());

UserEntity saved = userRepository.save(user);

eventPublisher.publishEvent(
new UserRegisteredEvent(
saved.getId(),
saved.getEmail()
)
);

return new UserDto(saved.getId(), saved.getEmail());
}
}

Memory sentence:

ApplicationEventPublisher publishes events into the Spring application context.


6. Event Class

Modern Spring events can be simple POJOs or records.

Example:

public record UserRegisteredEvent(
Long userId,
String email
) {
}

Older style may extend:

ApplicationEvent

Example:

public class UserRegisteredEvent extends ApplicationEvent {

private final Long userId;
private final String email;

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

public Long getUserId() {
return userId;
}

public String getEmail() {
return email;
}
}

For most modern app code, simple records are clean.

Memory sentence:

Event classes should describe what happened.


7. Event Listener

A listener reacts to events.

Example:

@Component
public class WelcomeEmailListener {

private final EmailService emailService;

public WelcomeEmailListener(EmailService emailService) {
this.emailService = emailService;
}

@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}
}

When UserRegisteredEvent is published, Spring calls this method.

Memory sentence:

@EventListener marks a method that reacts to an event.


8. Multiple Listeners

One event can have many listeners.

@Component
public class AuditListener {

@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
System.out.println("Audit user registered: " + event.userId());
}
}
@Component
public class AdminNotificationListener {

@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
System.out.println("Notify admin about user: " + event.email());
}
}

One publish:

eventPublisher.publishEvent(new UserRegisteredEvent(1L, "user@example.com"));

Can trigger:

WelcomeEmailListener
AuditListener
AdminNotificationListener

Memory sentence:

One event can have many listeners.


9. Event Flow

Service method

eventPublisher.publishEvent(event)

Spring ApplicationEventMulticaster

matching @EventListener methods

listener logic runs

Simple picture:

UserRegistrationService
publishes UserRegisteredEvent

WelcomeEmailListener
AuditListener
AdminNotificationListener

Memory sentence:

Spring dispatches published events to matching listeners.


10. Are Spring Events Synchronous?

By default, Spring event listeners are synchronous.

Meaning:

publishEvent() waits until listeners finish.

Example:

eventPublisher.publishEvent(new UserRegisteredEvent(...));

If the listener sends email and takes 3 seconds:

publishEvent() may also take 3 seconds.

Important:

Default Spring events are not automatically async.

Memory sentence:

By default, Spring events are synchronous.


11. Synchronous Event Consequence

If listener throws exception:

@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
throw new RuntimeException("Email failed");
}

Then the publisher may also fail because the listener is running in the same thread.

If the publisher method is transactional, this can affect the transaction.

Memory sentence:

In synchronous events, listener failure can affect the publisher.


12. Should Listener Failure Break Main Use Case?

Ask this design question:

If welcome email fails, should user registration fail?

Sometimes yes:

Payment validation failed
Fraud check failed
Mandatory domain rule failed

Often no:

welcome email failed
audit logging failed temporarily
notification failed
analytics event failed

If failure should not break main use case, handle it carefully.

Memory sentence:

Decide whether listener failure should fail the main action.


13. Transaction Problem

Imagine this service:

@Transactional
public UserDto register(RegisterUserRequest request) {
UserEntity user = userRepository.save(new UserEntity(request.email()));

eventPublisher.publishEvent(
new UserRegisteredEvent(user.getId(), user.getEmail())
);

return new UserDto(user.getId(), user.getEmail());
}

Default event listener:

@EventListener
public void sendWelcomeEmail(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}

Problem:

The event listener may run before the transaction commits.

If the transaction later rolls back:

Email may already be sent for a user that was not committed.

Memory sentence:

Normal @EventListener may run before transaction commit.


14. Transactional Event Listener

Use:

@TransactionalEventListener

when the listener should run based on transaction outcome.

Example:

@Component
public class WelcomeEmailListener {

private final EmailService emailService;

public WelcomeEmailListener(EmailService emailService) {
this.emailService = emailService;
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onUserRegistered(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}
}

This means:

Run this listener after the transaction successfully commits.

Memory sentence:

@TransactionalEventListener(AFTER_COMMIT) runs only after successful commit.


15. Transaction Phases

Common transaction phases:

BEFORE_COMMIT
AFTER_COMMIT
AFTER_ROLLBACK
AFTER_COMPLETION

Meaning:

PhaseMeaning
BEFORE_COMMITbefore transaction commits
AFTER_COMMITafter successful commit
AFTER_ROLLBACKafter rollback
AFTER_COMPLETIONafter commit or rollback

Most common for side effects:

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)

Memory sentence:

Use AFTER_COMMIT for side effects that should happen only after successful database commit.


16. @EventListener vs @TransactionalEventListener

Topic@EventListener@TransactionalEventListener
Transaction-awareno, normal event listeneryes
Default timingimmediately when publishedbound to transaction phase
Common usesimple in-memory reactionafter-commit side effects
Runs without transactionyesby default, no
Examplelogging simple eventsend email after user saved

Memory sentence:

Use normal events for simple reactions; transactional events for transaction-sensitive side effects.


17. Fallback Execution

@TransactionalEventListener normally expects a transaction.

If no transaction exists, it may not run.

There is an option:

@TransactionalEventListener(
phase = TransactionPhase.AFTER_COMMIT,
fallbackExecution = true
)

Meaning:

If there is no transaction, run anyway.

Use carefully.

Memory sentence:

fallbackExecution = true lets transactional listener run even without a transaction.


18. Async Events

Sometimes listener work should happen asynchronously.

Example:

send email
sync CRM
send analytics event
generate PDF
notify external system

Use @Async on listener:

@Component
public class WelcomeEmailListener {

private final EmailService emailService;

public WelcomeEmailListener(EmailService emailService) {
this.emailService = emailService;
}

@Async
@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}
}

Enable async:

@Configuration
@EnableAsync
public class AsyncConfig {
}

Memory sentence:

@Async can make event listener work run in another thread.


19. Async Event Trap

@Async also uses Spring proxy/interception.

Important:

The listener bean must be a Spring bean.
@EnableAsync must be enabled.
Async execution uses another thread.
ThreadLocal context may not be available automatically.
Exceptions are handled differently.

Example issue:

SecurityContext may not automatically be available.
Transaction context is not the same thread.
MDC/logging context may not automatically propagate.

Memory sentence:

Async listeners do not run in the same thread context.


20. Async + Transactional Event

Common pattern:

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onUserRegistered(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}

Meaning:

After transaction commits, handle event asynchronously.

Good for:

emails
notifications
external sync
analytics

But remember:

This is still in-process async.
If the app crashes, the event may be lost.

Memory sentence:

Async transactional events are useful, but not durable messaging.


21. Spring Events Are In-Process

Spring application events are in-process.

Meaning:

inside one running application instance
inside one JVM
not automatically stored
not automatically retried
not automatically shared with other services

If the app crashes after commit but before async listener finishes:

event may be lost

For durable messaging, use:

Kafka
RabbitMQ
SQS
database outbox pattern
message broker

Memory sentence:

Spring events are not a replacement for durable messaging.


22. Good Event Design

Good event names:

UserRegisteredEvent
TaskCompletedEvent
InvoicePaidEvent
DocumentUploadedEvent
ClientCreatedEvent
PasswordResetRequestedEvent

Bad event names:

SendEmailEvent
CallAuditServiceEvent
DoNotificationEvent

Why?

Good event says what happened.
Bad event says what listener should do.

Memory sentence:

Name events after facts, not commands.


23. Event Should Contain What?

Good event payload:

public record UserRegisteredEvent(
Long userId,
String email
) {
}

Usually include:

IDs
small immutable data
business facts
timestamp if useful
tenant ID if needed

Avoid:

large entity graphs
managed JPA entities
sensitive data
raw password
too much mutable state

Memory sentence:

Events should be small, immutable facts.


24. Avoid Passing JPA Entities in Events

Bad:

public record UserRegisteredEvent(UserEntity user) {
}

Problems:

entity may be lazy-loaded later
entity may be detached
listener may accidentally modify entity
payload becomes large
transaction boundary becomes confusing

Better:

public record UserRegisteredEvent(
Long userId,
String email
) {
}

Memory sentence:

Prefer IDs and simple values over JPA entities in events.


25. Event Example: Task Created

Event:

public record TaskCreatedEvent(
Long taskId,
Long tenantId,
String title
) {
}

Publisher:

@Service
public class TaskService {

private final TaskRepository taskRepository;
private final ApplicationEventPublisher eventPublisher;

public TaskService(
TaskRepository taskRepository,
ApplicationEventPublisher eventPublisher
) {
this.taskRepository = taskRepository;
this.eventPublisher = eventPublisher;
}

@Transactional
public TaskDto create(CreateTaskRequest request, CurrentUser currentUser) {
TaskEntity task = new TaskEntity(
request.title(),
currentUser.tenantId()
);

TaskEntity saved = taskRepository.save(task);

eventPublisher.publishEvent(
new TaskCreatedEvent(
saved.getId(),
saved.getTenantId(),
saved.getTitle()
)
);

return new TaskDto(saved.getId(), saved.getTitle());
}
}

Listener:

@Component
public class TaskCreatedAuditListener {

private final AuditService auditService;

public TaskCreatedAuditListener(AuditService auditService) {
this.auditService = auditService;
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void audit(TaskCreatedEvent event) {
auditService.recordTaskCreated(
event.taskId(),
event.tenantId(),
event.title()
);
}
}

Memory sentence:

Publish domain facts from services and react in listeners.


26. Event Example: Password Reset

Event:

public record PasswordResetRequestedEvent(
Long userId,
String email,
String resetToken
) {
}

Publisher:

@Transactional
public void requestPasswordReset(String email) {
UserEntity user = userRepository.findByEmail(email)
.orElseThrow();

String token = resetTokenService.createToken(user);

eventPublisher.publishEvent(
new PasswordResetRequestedEvent(
user.getId(),
user.getEmail(),
token
)
);
}

Listener:

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendResetEmail(PasswordResetRequestedEvent event) {
emailService.sendPasswordResetEmail(
event.email(),
event.resetToken()
);
}

Important:

Be careful with sensitive tokens.
Do not log them.

Memory sentence:

Events can trigger emails after commit, but sensitive data must be protected.


27. Listener Ordering

If several listeners exist, order may matter.

Use:

@Order(1)

Example:

@Component
public class FirstListener {

@Order(1)
@EventListener
public void handle(UserRegisteredEvent event) {
}
}
@Component
public class SecondListener {

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

But try not to rely heavily on listener order.

If order is business-critical, direct orchestration may be clearer.

Memory sentence:

Listener order exists, but business-critical order should be explicit.


28. Returning Events from Listener

An @EventListener method can return a value that becomes another event.

Example:

@EventListener
public WelcomeEmailEvent onUserRegistered(UserRegisteredEvent event) {
return new WelcomeEmailEvent(event.userId(), event.email());
}

This can be useful, but can also make flow harder to understand.

For exam, remember:

Event listeners can trigger additional events.

Memory sentence:

Listener return values can publish follow-up events, but use carefully.


29. Conditional Event Listener

@EventListener can use conditions.

Example:

@EventListener(condition = "#event.email.endsWith('@example.com')")
public void handleExampleDomainUser(UserRegisteredEvent event) {
// only example.com users
}

This uses SpEL.

Useful sometimes, but do not overuse.

Memory sentence:

Event listeners can be conditional.


30. Events vs Method Calls

Use direct method calls when:

the action is required immediately
the caller needs the result
the order is important
the logic is part of the same use case
failure should fail the main operation

Use events when:

something happened
other components may react
publisher should not know all reactions
side effects can be separated
new listeners may be added later

Memory sentence:

Direct call for required work; event for reactions to something that happened.


31. Events vs Message Broker

Use Spring events for:

in-process decoupling
same application
simple side effects
local module communication
after-commit listeners

Use message broker for:

communication between services
durable delivery
retry
backpressure
event replay
distributed systems
event-driven architecture across apps

Memory sentence:

Spring events are local; brokers are distributed and durable.


32. Events and Hexagonal/Clean Architecture

Events can support clean architecture.

Application service:

does main use case
publishes domain event

Listeners/adapters:

send email
write audit log
notify external system
sync search index

This keeps the core use case cleaner.

But do not use events to hide unclear control flow.

Memory sentence:

Events can decouple modules, but too many events can hide flow.


33. Testing Event Publishing

Service test with mocked publisher:

class UserRegistrationServiceTest {

private final UserRepository userRepository = mock(UserRepository.class);
private final ApplicationEventPublisher eventPublisher =
mock(ApplicationEventPublisher.class);

private final UserRegistrationService service =
new UserRegistrationService(userRepository, eventPublisher);

@Test
void publishesUserRegisteredEvent() {
when(userRepository.save(any(UserEntity.class)))
.thenReturn(new UserEntity(1L, "user@example.com"));

service.register(new RegisterUserRequest("user@example.com"));

ArgumentCaptor<UserRegisteredEvent> captor =
ArgumentCaptor.forClass(UserRegisteredEvent.class);

verify(eventPublisher).publishEvent(captor.capture());

UserRegisteredEvent event = captor.getValue();

assertThat(event.userId()).isEqualTo(1L);
assertThat(event.email()).isEqualTo("user@example.com");
}
}

Memory sentence:

Unit tests can verify that a service publishes the right event.


34. Testing Event Listener

Listener unit test:

class WelcomeEmailListenerTest {

private final EmailService emailService = mock(EmailService.class);

private final WelcomeEmailListener listener =
new WelcomeEmailListener(emailService);

@Test
void sendsWelcomeEmail() {
listener.onUserRegistered(
new UserRegisteredEvent(1L, "user@example.com")
);

verify(emailService).sendWelcomeEmail("user@example.com");
}
}

This does not test Spring event dispatching.

It tests listener logic.

Memory sentence:

Listener logic can be tested as a normal unit test.


35. Testing Spring Event Integration

Integration test:

@SpringBootTest
class EventIntegrationTest {

@Autowired
private ApplicationEventPublisher eventPublisher;

@MockitoBean
private EmailService emailService;

@Test
void listenerReceivesPublishedEvent() {
eventPublisher.publishEvent(
new UserRegisteredEvent(1L, "user@example.com")
);

verify(emailService).sendWelcomeEmail("user@example.com");
}
}

This proves:

Spring dispatches event to listener
listener calls email service

Memory sentence:

Use Spring test context to test real event dispatching.


36. Testing Transactional Event Listener

Transactional event listeners are harder to test because they depend on commit/rollback.

Example approach:

@SpringBootTest
class TransactionalEventIntegrationTest {

@Autowired
private UserRegistrationService userRegistrationService;

@MockitoBean
private EmailService emailService;

@Test
void sendsEmailAfterCommit() {
userRegistrationService.register(
new RegisterUserRequest("user@example.com")
);

verify(emailService).sendWelcomeEmail("user@example.com");
}
}

But be careful:

If the test method itself is @Transactional and rolls back,
AFTER_COMMIT listener may not run.

Memory sentence:

AFTER_COMMIT listeners need a real commit to run.


37. Common Testing Trap

Bad test:

@SpringBootTest
@Transactional
class TransactionalEventTest {

@Test
void sendsEmailAfterCommit() {
userRegistrationService.register(...);

verify(emailService).sendWelcomeEmail(...);
}
}

Problem:

The test transaction rolls back after the test.
AFTER_COMMIT listener may not run.

Fix options:

do not put @Transactional on the test
use TestTransaction to end/commit transaction manually
test listener logic separately
use integration test design carefully

Memory sentence:

Do not expect AFTER_COMMIT event to run inside a rolled-back test transaction.


38. Common Exam Traps

Trap 1

Spring events are in-process events.


Trap 2

By default, Spring event listeners are synchronous.


Trap 3

publishEvent() may wait for listeners to finish.


Trap 4

Listener exceptions can affect the publisher in synchronous mode.


Trap 5

Normal @EventListener is not transaction-phase aware.


Trap 6

@TransactionalEventListener(AFTER_COMMIT) runs after successful commit.


Trap 7

AFTER_COMMIT listeners may not run if transaction rolls back.


Trap 8

@TransactionalEventListener may not run without a transaction unless fallbackExecution = true.


Trap 9

@Async needs @EnableAsync.


Trap 10

Async listeners run in a different thread.


Trap 11

Spring events are not durable messaging.


Trap 12

Do not pass large JPA entities in events.


Trap 13

Name events after what happened, not what should be done.


Trap 14

Use direct method calls when the caller needs the result or failure should stop the use case.


Trap 15

Use message brokers when cross-service durable delivery is required.


39. Real Exam Question: Spring Event

Question:

What is a Spring application event?

Answer:

A Spring application event is an object published inside the application context to signal that something happened. Spring dispatches the event to matching listeners.


40. Real Exam Question: ApplicationEventPublisher

Question:

What is ApplicationEventPublisher used for?

Answer:

It is used to publish events into the Spring application context by calling publishEvent(...).


41. Real Exam Question: @EventListener

Question:

What does @EventListener do?

Answer:

It marks a Spring bean method as an event listener. Spring calls the method when a matching event is published.


42. Real Exam Question: Sync or Async

Question:

Are Spring events asynchronous by default?

Answer:

No. By default, Spring event listeners are synchronous.


43. Real Exam Question: Transactional Event

Question:

What is @TransactionalEventListener used for?

Answer:

It is used to bind event listener execution to a transaction phase, such as after commit or after rollback.


44. Real Exam Question: AFTER_COMMIT

Question:

What does TransactionPhase.AFTER_COMMIT mean?

Answer:

The listener runs only after the surrounding transaction successfully commits.


45. Real Exam Question: Async Listener

Question:

What is needed to use @Async on event listeners?

Answer:

The listener method must be on a Spring bean, and async support must be enabled with @EnableAsync.


46. Interview Answer

Question:

Explain Spring events in simple words.

Good answer:

Spring events allow one part of the application to publish that something happened, and other Spring beans can listen and react. For example, after a user registers, the service can publish UserRegisteredEvent, and separate listeners can send a welcome email, write an audit log, or notify an admin. This reduces direct coupling between the main service and side effects.


47. Interview Answer

Question:

Are Spring events synchronous or asynchronous?

Good answer:

By default, Spring events are synchronous. The publishEvent() call waits until the listeners finish. If I want asynchronous processing, I can use @Async with @EnableAsync or configure the event multicaster, but I must remember that async listeners run in a different thread and are not durable messaging.


48. Interview Answer

Question:

Why use @TransactionalEventListener(AFTER_COMMIT)?

Good answer:

I use AFTER_COMMIT when a side effect should happen only after the database transaction successfully commits. For example, sending a welcome email after user registration should usually happen only if the user was actually saved. A normal @EventListener may run before the transaction commits, which can cause side effects for rolled-back data.


49. Interview Answer

Question:

What is the difference between Spring events and Kafka?

Good answer:

Spring events are local, in-process events inside one application context. They are useful for decoupling modules inside the same application. Kafka is a distributed durable message broker used for communication between services, retries, backpressure, and event-driven architecture across applications. Spring events are not durable and can be lost if the application crashes.


50. Tiny Code Practice

Create an event for task completion.

Event:

public record TaskCompletedEvent(
Long taskId,
Long tenantId,
String title
) {
}

Publisher:

@Service
public class TaskService {

private final TaskRepository taskRepository;
private final ApplicationEventPublisher eventPublisher;

public TaskService(
TaskRepository taskRepository,
ApplicationEventPublisher eventPublisher
) {
this.taskRepository = taskRepository;
this.eventPublisher = eventPublisher;
}

@Transactional
public TaskDto complete(Long taskId) {
TaskEntity task = taskRepository.findById(taskId)
.orElseThrow();

task.complete();

eventPublisher.publishEvent(
new TaskCompletedEvent(
task.getId(),
task.getTenantId(),
task.getTitle()
)
);

return new TaskDto(task.getId(), task.getTitle(), task.getStatus());
}
}

Listener:

@Component
public class TaskCompletedAuditListener {

private final AuditService auditService;

public TaskCompletedAuditListener(AuditService auditService) {
this.auditService = auditService;
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void audit(TaskCompletedEvent event) {
auditService.recordTaskCompleted(
event.taskId(),
event.tenantId(),
event.title()
);
}
}

Question:

Why use AFTER_COMMIT?

Answer:

Because the audit side effect should happen only after the task completion is successfully committed.

51. Tiny Bug Practice 1

Problem:

@Transactional
public void registerUser(RegisterUserRequest request) {
UserEntity user = userRepository.save(...);

eventPublisher.publishEvent(new UserRegisteredEvent(user.getId(), user.getEmail()));

throw new RuntimeException("Something failed");
}

Listener:

@EventListener
public void sendEmail(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}

Question:

What is dangerous?

Answer:

The normal @EventListener may run before the transaction commits. The email may be sent even though the transaction later rolls back.

Better:

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)

52. Tiny Bug Practice 2

Problem:

@Async
@EventListener
public void handle(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.email());
}

But it still runs synchronously.

Question:

What might be missing?

Answer:

@EnableAsync may be missing.


53. Tiny Bug Practice 3

Problem:

public record UserRegisteredEvent(UserEntity user) {
}

Question:

Why is this not ideal?

Answer:

Passing a JPA entity in an event can cause lazy loading issues, detached entity problems, accidental modifications, large payloads, and confusing transaction boundaries. Prefer IDs and simple values.


54. Tiny Bug Practice 4

Problem:

public record SendWelcomeEmailEvent(String email) {
}

Question:

Why is this event name not ideal?

Answer:

It names the action a listener should perform, not the fact that happened. Better:

public record UserRegisteredEvent(Long userId, String email) {
}

Practice Questions and Answers

Question 1

What is a Spring event?

Answer:

A Spring event is an object published inside the application context to signal that something happened.


Question 2

Why use events?

Answer:

Events reduce direct coupling between the main business action and side effects or reactions.


Question 3

What does ApplicationEventPublisher do?

Answer:

ApplicationEventPublisher publishes events by calling publishEvent(...).


Question 4

What does @EventListener do?

Answer:

@EventListener marks a Spring bean method as a listener for a matching event type.


Question 5

Are Spring events synchronous by default?

Answer:

Yes. By default, Spring event listeners are synchronous.


Question 6

What happens if a synchronous listener throws an exception?

Answer:

The exception can propagate back to the publisher and may cause the publisher operation to fail.


Question 7

Why can normal @EventListener be dangerous inside a transaction?

Answer:

Because the listener may run before the transaction commits. If the transaction later rolls back, side effects may already have happened.


Question 8

What does @TransactionalEventListener do?

Answer:

It binds listener execution to a transaction phase.


Question 9

What does AFTER_COMMIT mean?

Answer:

The listener runs after the transaction successfully commits.


Question 10

What does AFTER_ROLLBACK mean?

Answer:

The listener runs after the transaction rolls back.


Question 11

What does fallbackExecution = true mean?

Answer:

It allows the transactional event listener to run even if no transaction exists.


Question 12

How can I make an event listener asynchronous?

Answer:

Annotate the listener with @Async.


Question 13

What is needed for @Async?

Answer:

Async support must be enabled with @EnableAsync, and the listener must be a Spring bean.


Question 14

Are Spring events durable messaging?

Answer:

No. Spring events are local in-process events and are not durable messaging.


Question 15

When should I use Kafka/RabbitMQ instead of Spring events?

Answer:

Use a broker when communication is between services, must be durable, must be retried, or must survive application crashes.


Question 16

What should event names describe?

Answer:

Event names should describe what happened, such as UserRegisteredEvent, not what should be done.


Question 17

What should an event payload contain?

Answer:

Small immutable data such as IDs, tenant IDs, email, status, timestamp, or other business facts.


Question 18

Why avoid JPA entities in events?

Answer:

Because JPA entities can cause lazy loading, detached entity, accidental modification, large payload, and transaction boundary problems.


Question 19

How do I test event publishing?

Answer:

Mock ApplicationEventPublisher, call the service, capture the published event with ArgumentCaptor, and assert its fields.


Question 20

How do I test listener logic?

Answer:

Instantiate the listener with mocked dependencies and call the listener method directly, then verify the expected side effect.

Final Memory Sentences

  • Spring events signal that something happened.
  • ApplicationEventPublisher publishes events.
  • @EventListener receives events.
  • One event can have many listeners.
  • By default, Spring events are synchronous.
  • publishEvent() waits for synchronous listeners.
  • Listener exceptions can affect the publisher.
  • Normal @EventListener may run before transaction commit.
  • @TransactionalEventListener binds listener execution to transaction phase.
  • AFTER_COMMIT runs after successful commit.
  • AFTER_ROLLBACK runs after rollback.
  • fallbackExecution = true runs even without a transaction.
  • @Async can make listeners asynchronous.
  • @EnableAsync is needed for async support.
  • Async listeners run in a different thread context.
  • Spring events are in-process, not durable messaging.
  • Use Kafka/RabbitMQ/outbox for durable distributed events.
  • Name events after facts, not commands.
  • Keep event payloads small and immutable.
  • Prefer IDs and simple values over JPA entities.
  • Direct call for required work; event for reactions.