or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

aop-interceptors.mddao-support.mddeclarative-transactions.mdexception-hierarchy.mdindex.mdjca-support.mdprogrammatic-transactions.mdreactive-transactions.mdtransaction-events.mdtransaction-managers.mdtransaction-synchronization.md
tile.json

transaction-events.mddocs/

Transactional Event Listeners

Transactional event listeners enable Spring events to be published and handled at specific transaction phases. This provides a clean way to decouple transactional operations from their side effects.

Capabilities

@TransactionalEventListener

Marks a method as a listener for application events that should be handled at a specific transaction phase.

/**
 * Marks a method as a listener for application events tied to transaction phases.
 * The listener method is invoked according to the specified TransactionPhase.
 */
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EventListener
public @interface TransactionalEventListener {

    /**
     * The transaction phase in which the event listener should be invoked.
     * Default is AFTER_COMMIT.
     */
    TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;

    /**
     * Whether the event listener should be invoked if no transaction is running.
     * Default is false (listener is not invoked without a transaction).
     */
    boolean fallbackExecution() default false;

    /**
     * Alias for classes().
     */
    @AliasFor(annotation = EventListener.class, attribute = "classes")
    Class<?>[] value() default {};

    /**
     * The event classes this listener handles.
     */
    @AliasFor(annotation = EventListener.class, attribute = "classes")
    Class<?>[] classes() default {};

    /**
     * SpEL expression used for making the event handling conditional.
     */
    @AliasFor(annotation = EventListener.class, attribute = "condition")
    String condition() default "";

    /**
     * An optional identifier for the listener, defaulting to the fully-qualified
     * signature of the declaring method.
     */
    @AliasFor(annotation = EventListener.class, attribute = "id")
    String id() default "";
}

Usage Examples:

import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.context.event.EventListener;

@Service
public class OrderEventListener {

    // Listen after successful commit (default)
    @TransactionalEventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        emailService.sendOrderConfirmation(event.getOrder());
    }

    // Listen after commit with explicit phase
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void sendNotification(OrderCreatedEvent event) {
        notificationService.notify(event.getOrder());
    }

    // Listen before commit (within transaction)
    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    public void validateBeforeCommit(OrderCreatedEvent event) {
        validator.finalValidation(event.getOrder());
    }

    // Listen after rollback
    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void handleRollback(OrderCreatedEvent event) {
        logger.warn("Order creation rolled back: {}", event.getOrder().getId());
        cleanupService.cleanup(event.getOrder());
    }

    // Listen after completion (commit or rollback)
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
    public void logCompletion(OrderCreatedEvent event) {
        auditLog.record("Order transaction completed: " + event.getOrder().getId());
    }

    // With fallback execution (also runs without transaction)
    @TransactionalEventListener(
        phase = TransactionPhase.AFTER_COMMIT,
        fallbackExecution = true
    )
    public void handleWithFallback(OrderCreatedEvent event) {
        // Runs after commit if in transaction, otherwise runs immediately
        cacheService.invalidate(event.getOrder().getId());
    }

    // With conditional execution using SpEL
    @TransactionalEventListener(
        phase = TransactionPhase.AFTER_COMMIT,
        condition = "#event.order.total > 1000"
    )
    public void handleHighValueOrder(OrderCreatedEvent event) {
        vipService.processHighValueOrder(event.getOrder());
    }

    // Multiple event types
    @TransactionalEventListener(
        phase = TransactionPhase.AFTER_COMMIT,
        classes = {OrderCreatedEvent.class, OrderUpdatedEvent.class}
    )
    public void handleOrderEvents(ApplicationEvent event) {
        if (event instanceof OrderCreatedEvent) {
            handleCreated((OrderCreatedEvent) event);
        } else if (event instanceof OrderUpdatedEvent) {
            handleUpdated((OrderUpdatedEvent) event);
        }
    }
}

TransactionPhase Enum

Defines the phase in which a transactional event listener should be invoked.

/**
 * Enumeration representing the phase in which transactional event listeners are invoked.
 */
public enum TransactionPhase {

    /**
     * Handle the event before transaction commit.
     * Listener is invoked within the transaction boundaries.
     */
    BEFORE_COMMIT,

    /**
     * Handle the event after the transaction committed successfully.
     * This is the default phase. Listener is invoked after successful commit.
     */
    AFTER_COMMIT,

    /**
     * Handle the event if the transaction rolled back.
     * Listener is invoked after rollback.
     */
    AFTER_ROLLBACK,

    /**
     * Handle the event after the transaction completed (committed or rolled back).
     * Listener is invoked after transaction completion, regardless of outcome.
     */
    AFTER_COMPLETION
}

Usage Pattern:

@Service
public class TransactionPhaseExamples {

    // BEFORE_COMMIT: Final validation within transaction
    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    public void beforeCommit(DataModifiedEvent event) {
        // Still within transaction, can query database
        // Exception here causes rollback
        performFinalValidation(event.getData());
    }

    // AFTER_COMMIT: External system integration
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void afterCommit(DataModifiedEvent event) {
        // Transaction committed, safe to call external systems
        externalApiClient.notifyChange(event.getData());
        searchIndexService.update(event.getData());
    }

    // AFTER_ROLLBACK: Cleanup and compensation
    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void afterRollback(DataModifiedEvent event) {
        // Transaction rolled back, perform cleanup
        logger.error("Transaction failed for: {}", event.getData().getId());
        compensationService.compensate(event.getData());
    }

    // AFTER_COMPLETION: Always executed
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
    public void afterCompletion(DataModifiedEvent event) {
        // Runs after commit OR rollback
        metricsService.recordTransactionAttempt();
        resourceCleanup.cleanup(event.getData());
    }
}

Publishing Events

Events are published using Spring's ApplicationEventPublisher:

@Service
public class OrderService {

    private final ApplicationEventPublisher eventPublisher;
    private final OrderRepository orderRepository;

    public OrderService(ApplicationEventPublisher eventPublisher,
                       OrderRepository orderRepository) {
        this.eventPublisher = eventPublisher;
        this.orderRepository = orderRepository;
    }

    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = new Order(request);
        orderRepository.save(order);

        // Publish event within transaction
        eventPublisher.publishEvent(new OrderCreatedEvent(this, order));

        return order;
    }

    @Transactional
    public void cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId);
        order.setStatus(OrderStatus.CANCELLED);
        orderRepository.save(order);

        eventPublisher.publishEvent(new OrderCancelledEvent(this, order));
    }
}

Event Classes

// Simple event class
public class OrderCreatedEvent extends ApplicationEvent {

    private final Order order;

    public OrderCreatedEvent(Object source, Order order) {
        super(source);
        this.order = order;
    }

    public Order getOrder() {
        return order;
    }
}

// Or without extending ApplicationEvent (Spring 4.2+)
public class OrderCancelledEvent {

    private final Order order;

    public OrderCancelledEvent(Order order) {
        this.order = order;
    }

    public Order getOrder() {
        return order;
    }
}

Common Patterns

Separating Business Logic from Side Effects

@Service
public class UserService {

    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public User registerUser(UserRegistration registration) {
        // Core business logic
        User user = new User(registration);
        userRepository.save(user);

        // Publish event - listeners handle side effects
        eventPublisher.publishEvent(new UserRegisteredEvent(user));

        return user;
    }
}

@Component
public class UserRegistrationHandlers {

    // Send welcome email after commit
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void sendWelcomeEmail(UserRegisteredEvent event) {
        emailService.sendWelcomeEmail(event.getUser());
    }

    // Update search index after commit
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void updateSearchIndex(UserRegisteredEvent event) {
        searchService.indexUser(event.getUser());
    }

    // Notify analytics after commit
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void notifyAnalytics(UserRegisteredEvent event) {
        analyticsService.trackUserRegistration(event.getUser());
    }
}

Conditional Event Handling

@Component
public class OrderEventHandlers {

    // Only handle high-value orders
    @TransactionalEventListener(
        phase = TransactionPhase.AFTER_COMMIT,
        condition = "#event.order.total > 1000"
    )
    public void handleHighValueOrder(OrderCreatedEvent event) {
        vipService.notifyHighValueOrder(event.getOrder());
    }

    // Only handle orders with specific status
    @TransactionalEventListener(
        phase = TransactionPhase.AFTER_COMMIT,
        condition = "#event.order.status.name() == 'PREMIUM'"
    )
    public void handlePremiumOrder(OrderCreatedEvent event) {
        premiumProcessingService.process(event.getOrder());
    }

    // Complex condition
    @TransactionalEventListener(
        phase = TransactionPhase.AFTER_COMMIT,
        condition = "#event.order.customer.vip and #event.order.total > 500"
    )
    public void handleVipOrder(OrderCreatedEvent event) {
        vipOrderService.expedite(event.getOrder());
    }
}

Error Handling and Compensation

@Component
public class PaymentEventHandlers {

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void processPayment(OrderCreatedEvent event) {
        try {
            paymentGateway.charge(event.getOrder());
        } catch (PaymentException e) {
            // Payment failed after order committed
            // Publish compensation event
            eventPublisher.publishEvent(
                new PaymentFailedEvent(event.getOrder(), e)
            );
        }
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void handleOrderRollback(OrderCreatedEvent event) {
        // Order creation rolled back, cleanup any pre-allocated resources
        resourceCleanupService.cleanup(event.getOrder());
    }
}

Asynchronous Event Processing

@Component
public class AsyncEventHandlers {

    // Process event asynchronously after commit
    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void sendEmailAsync(OrderCreatedEvent event) {
        // Runs in separate thread after transaction commits
        emailService.sendOrderConfirmation(event.getOrder());
    }

    // Enable async with @EnableAsync in configuration
}

@Configuration
@EnableAsync
public class AsyncConfig {
    // Async configuration
}

Event Ordering

@Component
public class OrderedEventHandlers {

    // Execute first (lower order value = higher priority)
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Order(1)
    public void firstHandler(OrderCreatedEvent event) {
        // Executed first
        criticalService.process(event.getOrder());
    }

    // Execute second
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Order(2)
    public void secondHandler(OrderCreatedEvent event) {
        // Executed second
        lessCriticalService.process(event.getOrder());
    }
}

Notes

  • @TransactionalEventListener extends Spring's @EventListener with transaction awareness
  • Default phase is AFTER_COMMIT - most common use case
  • BEFORE_COMMIT listeners execute within transaction boundaries
  • AFTER_COMMIT listeners only execute if transaction commits successfully
  • AFTER_ROLLBACK listeners only execute if transaction rolls back
  • AFTER_COMPLETION listeners execute regardless of commit/rollback
  • Set fallbackExecution = true to execute listeners even without a transaction
  • Exceptions in BEFORE_COMMIT listeners cause transaction rollback
  • Exceptions in other phase listeners don't affect the transaction
  • Use SpEL expressions in condition attribute for conditional execution
  • Events are synchronous by default, use @Async for asynchronous processing
  • Multiple listeners for same event execute in undefined order unless @Order is used
  • Transactional event listeners require an active PlatformTransactionManager bean