Spring Event

July 19, 2026

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 UserService thì 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 trong UserService.

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

Spring Event Flow Execution

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()));
}