Spring Event là một cơ chế cho phép các component trong ứng dụng Spring giao tiếp với nhau theo mô hình Publish-Subscriber (hoặc Observer Pattern) mà không bị phụ thuộc trực tiếp (decoupled) vào nhau.
Thay vì Service A phải nhúng (inject) Service B, Service C, Service D vào để gọi trực tiếp, Service A chỉ cần bắn đi một Event (Publish). Các service khác muốn xử lý chỉ cần lắng nghe (Listen/Subscribe) event đó.
1. Spring Event?
1.1. Khi nào nên dùng Spring Event?
Bạn nên dùng Event khi muốn tách rời logic chính và logic phụ (decoupling):
-
Tách logic phụ thuộc: Sau khi UserService.register() tạo tài khoản thành công, bạn cần:
- Gửi email chào mừng.
- Tạo mã coupon giảm giá.
- Bắn notification về app.
- Gửi log sang hệ thống analytics.
(Nếu viết chung vào
UserServicethì class này sẽ cực kỳ phình to và vi phạm nguyên lý Single Responsibility).
-
Dễ mở rộng (Extensibility): Mai mốt dự án muốn làm thêm tính năng "
Tặng 100 điểm thưởng cho user mới", bạn chỉ cần tạo thêm 1 Listener mới mà không sửa 1 dòng code nào trongUserService.
1.2. Spring Event là Đồng bộ hay Bất đồng bộ?
MẶC ĐỊNH LÀ ĐỒNG BỘ (Synchronous)
Rất nhiều người nhầm tưởng Event là chạy ngầm/chạy riêng thread. Nhưng mặc định trong Spring:
- Publisher và Listener chạy chung 1 Thread.
- Nếu Listener bị chậm (vd: mất 3 giây gửi email), thì Publisher phải đứng chờ 3 giây đó rồi mới chạy tiếp.
- Nếu Listener bị quăng
RuntimeException, cả Publisher cũng sẽ bị dính Exception đó (và có thể bị Rollback Transaction nếu có@Transactional). - Muốn quy định Listener A chạy xong thì Listener B mới được chạy thì thêm
@Order(số). Số càng nhỏ thì chạy càng trước, có thể dùng giá trị âm.
@Component
public class UserNotificationListeners {
// CHẠY ĐẦU TIÊN (Order = 1)
@Order(1)
@EventListener
public void createWelcomeCoupon(UserRegisteredEvent event) {
System.out.println("Step 1: Tạo coupon giảm giá cho user");
}
// CHẠY THỨ HAI (Order = 2)
@Order(2)
@EventListener
public void sendWelcomeEmail(UserRegisteredEvent event) {
System.out.println("Step 2: Gửi email chào mừng kèm coupon");
}
// CHẠY CUỐI CÙNG (Order = 3)
@Order(3)
@EventListener
public void logAudit(UserRegisteredEvent event) {
System.out.println("Step 3: Ghi log hoàn tất");
}
}
2. The Scenario
When a new user registers, we want to perform two secondary tasks:
- Send a welcome email (slow operation $\rightarrow$ should run asynchronously).
- Log audit data (must only run after the database transaction succeeds).
2.1. Create the Event Object
An event is just a plain Java class carrying whatever payload the listeners need.
public class UserRegisteredEvent {
private final String userId;
private final String email;
public UserRegisteredEvent(String userId, String email) {
this.userId = userId;
this.email = email;
}
public String getUserId() { return userId; }
public String getEmail() { return email; }
}
2.2. Publish the Event
Use Spring's ApplicationEventPublisher to fire the event inside your business logic.
@Service
public class UserService {
private final UserRepository userRepository;
private final ApplicationEventPublisher eventPublisher;
public UserService(UserRepository userRepository, ApplicationEventPublisher eventPublisher) {
this.userRepository = userRepository;
this.eventPublisher = eventPublisher;
}
@Transactional
public void registerUser(String email, String password) {
// 1. Save user to database
User user = userRepository.save(new User(email, password));
// 2. Publish event (Does NOT send email directly here)
UserRegisteredEvent event = new UserRegisteredEvent(user.getId(), user.getEmail());
eventPublisher.publishEvent(event);
// 3. Any error happening here will trigger DB rollback
}
}
2.3. Enable Async Processing
To run tasks on background threads, add @EnableAsync to your main class or any @Configuration class:
@Configuration
@EnableAsync
public class AsyncConfig {
// Basic setup: Spring will use a SimpleAsyncTaskExecutor by default,
// or you can configure a custom ThreadPoolTaskExecutor bean here.
}
2.3.1. Enable Virtual Thread via application.properties
If using Spring Boot 3.2+ and Java 21+ then enable virtual threads via application.properties.
spring.threads.virtual.enabled=true
When this configuration is enabled, Spring automatically configures @Async, and the default Task handlers switch to using the Virtual Thread Executor without you needing to write any additional Java code.
2.3.2. Custom Bean Virtual Thread Executor manually
@Configuration
@EnableAsync
public class AsyncVirtualThreadConfig {
@Bean(name = "virtualThreadExecutor")
public Executor virtualThreadExecutor() {
// Tạo Executor sử dụng Virtual Thread cho từng task
return Executors.newVirtualThreadPerTaskExecutor();
}
}
Use
@Async("virtualThreadExecutor")
public void handleAsyncTask(...) {
// Chạy trên Virtual Thread! Cực kỳ nhẹ và tối ưu I/O.
}
2.4. Enable Async Processing
Now create components to listen for UserRegisteredEvent.
@Component
public class UserRegisteredEventListener {
private static final Logger log = LoggerFactory.getLogger(UserRegisteredEventListener.class);
// =========================================================================
// LISTENER 1: Synchronous & Simple
// Runs on the SAME thread as UserService, immediately when publishEvent is called.
// =========================================================================
@EventListener
public void handleSyncLogging(UserRegisteredEvent event) {
log.info("[SYNC] Event received for user: {}", event.getEmail());
}
// =========================================================================
// LISTENER 2: Recommended Production Pattern for Emails/Push Notifications
// - AFTER_COMMIT: Waits until the database transaction is 100% successful.
// - @Async: Runs in a background thread pool so API response stays fast.
// =========================================================================
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleAsyncEmailNotification(UserRegisteredEvent event) {
log.info("[ASYNC] Sending welcome email to {} on thread {}",
event.getEmail(), Thread.currentThread().getName());
// Simulate email delay
try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
log.info("[ASYNC] Email successfully sent to {}", event.getEmail());
}
}
Flow Execution Summary

Important Gotcha with @TransactionalEventListener
If your listener method annotated with @TransactionalEventListener needs to write back to the database, you must use REQUIRES_NEW:
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW) // <--- Required to open a new DB transaction!
public void createWelcomeCoupon(UserRegisteredEvent event) {
// The main transaction has already committed/closed.
// Without REQUIRES_NEW, any save() call here will be ignored or throw an exception.
couponRepository.save(new Coupon(event.getUserId()));
}