DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Building Fault-Tolerant Kafka Consumers in Spring Boot Using Retry, DLQ, and Idempotent Code Patterns
  • Developing Saga Participant Code for Compensating Transactions
  • Zero-Downtime Deployments for Java Apps on Kubernetes
  • Stateless JWT Auth Microservice Architecture With Spring Boot 3 and Redis Sentinel

Trending

  • What Nobody Tells You About Multimodal Data Pipelines for AI Training
  • LLM Integration in Enterprise Applications: A Practical Guide
  • Lambda-Driven API Design: Building Composable Node.js Endpoints With Functional Primitives
  • Scaling Cloud Data Automation: A Practical Guide to Open Table Formats
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Deployment
  4. How to Reliably Implement Post-Commit Actions in Spring

How to Reliably Implement Post-Commit Actions in Spring

In this article, you will learn how to reliably implement post-commit actions in Spring Boot using a dedicated annotation, ensuring consistent behavior.

By 
Mario Casari user avatar
Mario Casari
·
Apr. 15, 26 · Tutorial
Likes (0)
Comment
Save
Tweet
Share
2.5K Views

Join the DZone community and get the full member experience.

Join For Free

Sometimes, in modern backend systems, you need to perform one or more actions after database inserts or updates. You may need to publish to a message broker, send an email, or trigger a workflow. If you perform those actions inside a database transaction that rolls back at the end, it is too late, because they are already started and cannot be cancelled, possibly creating inconsistencies. In Spring Boot, you can use @TransactionalEventListener to mark a listener that is triggered by the ApplicationEventPublisher's publish method to solve this problem. In this article, we will explain how this works.

The Problem

In this article, we are discussing the side effects associated with transactional code.  By side effects, we mean deliberate actions towards external services and resources. As an example, we can consider an application component that creates an order, saves it in the database, and then sends an email:

Java
 
@Transactional public void createOrder(Order order) {    
  orderRepository.save(order);    
  emailService.sendOrderConfirmation(order.getId()); 
}


In normal conditions, this works as expected: the order is persisted in the database, and the email is sent to the recipient. However, suppose the transaction fails for some reason. In this case, you still send the email, but the order does not exist in the database. 

In the next section, you will see an approach to solving this problem by using the @TransactionalEventListener annotation.

The Solution: @TransactionalEventListener

Spring provides the ApplicationEventPublisher class to decouple logic through application events using an implementation of the Observer pattern. By an ApplicationEventPublisher object, you can send an event in one part of the application:

Java
 
private final ApplicationEventPublisher publisher; 
publisher.publishEvent(new OrderCreatedEvent(order.getId()));


And catch it with a listener in another part:

@EventListener public void handle(OrderCreatedEvent event) {    emailService.sendOrderConfirmation(event.getOrderId()); }

This improves things from the architectural standpoint, but @EventListener executes immediately, even if the transaction rolls back. This way, your system will still suffer inconsistencies.

Replacing @EventListener with the @TransactionalEventListener annotation, though, you will get the event listener to run only after a configured phase of the transaction. By default, it runs after a successful commit. 

Java
 
@TransactionalEventListener public void handle(OrderCreatedEvent event) { 
  emailService.sendOrderConfirmation(event.getOrderId()); 
}


In the above code, you have the following execution steps in case of a successful commit:

  1. The order is saved
  2. The event is published
  3. The transaction commits
  4. The listener executes
  5. The email is sent

If the transaction fails, the listener never runs, and no inconsistency is left.

Implementation

To complete the example shown in the previous section, you should first define a simple event class. Events should be lightweight with only identifiers rather than full entities. In the following code snippet, you have an event related to the creation of an order, and its definition class contains only the order ID.

Java
 
public class OrderCreatedEvent {   
  
   private final Long orderId;     
  
   public OrderCreatedEvent(Long orderId) {        
     this.orderId = orderId;    
   }
  
   public Long getOrderId() {        
     return orderId;    
   } 
}


Then, you should publish the event inside a transactional method of a service class:

Java
 
@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final ApplicationEventPublisher publisher;

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

    @Transactional
    public void createOrder(Order order) {

        orderRepository.save(order);

        publisher.publishEvent(new OrderCreatedEvent(order.getId()));
    }
}


Note that the event is published inside the transaction. Then, you should handle the event after the commit phase by creating a listener:

Java
 
@Component
public class OrderEventListener {

    @TransactionalEventListener
    public void handle(OrderCreatedEvent event) {

        emailService.sendOrderConfirmation(event.getOrderId());

    }
}


This listener will execute only after the transaction commits successfully. Note that @TransactionalEventListener, without any additional configuration, runs after a successful commit. In the following section, you will see that you can modify the transaction phase in which the listener is triggered by using a specific parameter.

Transaction Phases

You can configure the listener to specify the transaction phase in which it should run. The default phase is AFTER_COMMIT:

  • BEFORE_COMMIT: Runs just before the transaction commits. It can be configured with @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT). It's useful when you need to finalize data before persistence completes
  • AFTER_COMMIT: Configured with @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT). It runs after a successful commit. It is best suited for sending emails, publishing events, updating caches, and triggering workflows
  • AFTER_ROLLBACK: Configured with @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK), It runs only if the transaction fails and rolls back. It can be used for clean-up operations or for executing compensating actions
  • AFTER_COMPLETION: Configured with @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION), it runs after the transaction regardless of its outcome. It can be used for cleaning up or executing compensating actions. It could also be useful for monitoring or auditing.

Asynchronous Execution

The actions performed after the transaction completion are usually external calls, like:

  • Sending emails
  • Calling external APIs
  • Sending Kafka messages
  • Running analytics

They can be slow and may cause the program to become unresponsive. To avoid that, you can combine @TransactionalEventListener with @Async, like in the following example:

Java
 
@Async
@TransactionalEventListener 
public void handle(OrderCreatedEvent event) {   
  analyticsService.trackOrder(event.getOrderId()); 
}


This way, the transaction completes without delay, while the heavy work is handled in the background.

How it works

Internally, Spring registers a transaction synchronization mechanism with the transaction manager. During the transaction execution, Spring performs the following steps:

  1. Stores the event
  2. Waits until the transaction completes
  3. Executes the listener in the configured phase

Best Practices 

A couple of best practices in using this feature:

  • Publish events from the service layer: Events should usually be triggered from transactional services, not controllers
  • Use for side effects only: @TransactionalEventListener is ideal for external communication, background processing, and integration with other systems. Avoid using it for core business logic

Conclusion

@TransactionalEventListener is a useful feature available in Spring Boot applications. It allows you to link actions to specific database transaction phases, like the after-commit phase. It helps in keeping a clean business logic and avoiding inconsistent states.

You can find the example code on GitHub.

Business logic Commit (data management) Spring Boot

Published at DZone with permission of Mario Casari. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Building Fault-Tolerant Kafka Consumers in Spring Boot Using Retry, DLQ, and Idempotent Code Patterns
  • Developing Saga Participant Code for Compensating Transactions
  • Zero-Downtime Deployments for Java Apps on Kubernetes
  • Stateless JWT Auth Microservice Architecture With Spring Boot 3 and Redis Sentinel

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook