Week 8 Day 2 — Spring Events
Goal
Today I want to understand Spring application events.
Main questions:
- What is a Spring event?
- Why use events?
- What is
ApplicationEventPublisher? - What is
@EventListener? - Are Spring events synchronous or asynchronous?
- What is a transactional event?
- What is
@TransactionalEventListener? - What does
AFTER_COMMITmean? - When should I use events?
- When should I not use events?
- 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:
ApplicationEventPublisherpublishes 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:
@EventListenermarks 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
@EventListenermay 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:
| Phase | Meaning |
|---|---|
BEFORE_COMMIT | before transaction commits |
AFTER_COMMIT | after successful commit |
AFTER_ROLLBACK | after rollback |
AFTER_COMPLETION | after commit or rollback |
Most common for side effects:
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
Memory sentence:
Use
AFTER_COMMITfor side effects that should happen only after successful database commit.
16. @EventListener vs @TransactionalEventListener
| Topic | @EventListener | @TransactionalEventListener |
|---|---|---|
| Transaction-aware | no, normal event listener | yes |
| Default timing | immediately when published | bound to transaction phase |
| Common use | simple in-memory reaction | after-commit side effects |
| Runs without transaction | yes | by default, no |
| Example | logging simple event | send 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 = truelets 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:
@Asynccan 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_COMMITlisteners 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_COMMITevent 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.
ApplicationEventPublisherpublishes events.@EventListenerreceives 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
@EventListenermay run before transaction commit. @TransactionalEventListenerbinds listener execution to transaction phase.AFTER_COMMITruns after successful commit.AFTER_ROLLBACKruns after rollback.fallbackExecution = trueruns even without a transaction.@Asynccan make listeners asynchronous.@EnableAsyncis 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.