Spring @Transactional Rollback Handling
Published: 2023-08-03 12:43:53


## Spring @Transactional Rollback Handling

![Photo by [James Harrison](https://unsplash.com/@jstrippa?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/programming?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)](https://cdn-images-1.medium.com/max/2000/1*0MiR7DOGFKcFByxAwgETGg.jpeg)

By default, the spring boot transaction is **auto-commit**. Every single SQL statement is in its own transaction and will commit after execution. Take a look at the below example, the product is inserted into the database even though an exception has been raised.

    public void createProduct() {  
        System.out.println("------ createProduct ------");
        Product prod = new Product();
        prod.setDescription("This is an example with runtime exception but no rollback.");
        prod.setPrice(10);
        prod.setTitle("First Product");
        productRepository.save(prod);
        System.out.println("First Product inserted.");

    throw new RuntimeException();
    }

![](https://cdn-images-1.medium.com/max/2768/1*_HiNJ_hTYBpYia51w8HIKQ.png)

This shouldn’t be the case. In some scenarios, multiple transactions are run in a logical unit of code. All the transactions should only commit when there is no exception occurring in between.

In Spring Boot, when **@Transactional** annotation is used, Spring Boot implicitly creates a proxy that will be creating a connection to the database. A transaction will be started and committed after the code has been executed errorless. Otherwise, it will roll back the changes if an exception occurred.

    **import** **java.sql.Connection**;
    
    **Connection** conn = dataSource.getConnection(); 
    
    **try** (connection) {

     *   // execute some SQL statements...*

     *   *// commit transaction
        conn.commit();
    
    } **catch** (**SQLException** e) {
        conn.rollback();
    }

## Example 1

In this example, we added **@Transactional** annotation to roll back the transaction if the exception occurred.

    **@Transactional**
    public void createProduct() {  
        System.out.println("------ createProduct ------");
        Product prod = new Product();
        prod.setDescription("This is an example with runtime exception and transactional annotation.");
        prod.setPrice(10);
        prod.setTitle("Second Product");
        productRepository.save(prod);
        System.out.println("Second Product inserted.");

        throw new RuntimeException();
    }

![](https://cdn-images-1.medium.com/max/2768/1*_HiNJ_hTYBpYia51w8HIKQ.png)

The above example proves that the ***@Transactional*** annotation can roll back the transaction if the exception occurs. Take note, Spring only rolled back on ***unchecked*** exceptions by default.

## Example 2a

This example shows that the ***checked*** exception will not be rolled back even though we have specified the **@Transactional **annotation.

    @Transactional
    public void createProduct() throws Exception{  
        System.out.println("------ createProduct ------");
        Product prod = new Product();
        prod.setDescription("This is an example with checked exception and transactional annotation.");
        prod.setPrice(10);
        prod.setTitle("Second Product");
        productRepository.save(prod);
        System.out.println("Second Product inserted.");
        throw new SQLException();
    }

![](https://cdn-images-1.medium.com/max/2892/1*rHmiyUG3kurzDH3xouC0fw.png)

## Example 2b

To roll back **checked** exceptions, we need to specify the ***rollbackFor***

    @Transactional( **rollbackFor** = SQLException.class)
    public void createProduct() throws Exception{  
        System.out.println("------ createProduct ------");
        Product prod = new Product();
        prod.setDescription("This is an example with checked exception and transactional annotation with rollbackFor.");
        prod.setPrice(10);
        prod.setTitle("Example 2b Product");
        productRepository.save(prod);
        System.out.println("Example 2b inserted.");
        throw new SQLException();
    }

![](https://cdn-images-1.medium.com/max/2892/1*rHmiyUG3kurzDH3xouC0fw.png)

We see that the transaction has rolled back successfully.

## Example 3

In this example, we added the try-catch block to see if the rollback still working.

    @Transactional
    public void createProduct() {
        try {
            System.out.println("------ createProduct ------");
            Product prod = new Product();
            prod.setDescription("This is an example with runtime exception, transactional annotation and try catch.");
            prod.setPrice(10);
            prod.setTitle("Example 3 Product");
            productRepository.save(prod);
            System.out.println("Example 3 Product inserted.");
            throw new RuntimeException();
        }catch (Exception e){
            System.out.println("Here we catch the exception.");
        }
    }

![](https://cdn-images-1.medium.com/max/2960/1*jLYgNeOC6UvukA4Nixq5vg.png)

It won’t work. This happened because we manually caught the exception and handle it. Therefore the current transaction is being executed normally and committed.

## Example 4

What about a **nested transaction**? From the example below, the *createProduct()*and *createOrder()* methods will insert a record into the database. The *createOrder()* method has thrown a runtime exception. Both methods have been annotated with the **@Transactional **annotation.

    //ProductController.java
    @Transactional
    public void createProduct() {
        System.out.println("------ createProduct ------");
        Product prod = new Product();
        prod.setDescription("This is createProduct method.");
        prod.setPrice(10);
        prod.setTitle("Create Product");
        productRepository.save(prod);
        orderController.createOrder();
    }

    //OrderController.java
    @Transactional
    public void createOrder() {
        System.out.println("------ createOrder ------");
        Order order = new Order();
        order.setTitle("Create Order");
        order.setDescription("This is createOrder method with runtime exception");
        orderRepository.save(order);  
        throw new RuntimeException("Create Order RuntimeException");
    }

![](https://cdn-images-1.medium.com/max/2960/1*jLYgNeOC6UvukA4Nixq5vg.png)

*createProduct()*and *createOrder()* transactions were rolled back. Even though the runtime exception has happened in the *createOrder()* method. However, because it doesn’t handle the exception, this led to the *createProduct()* transaction also rollback.

## Example 5

Let’s add in the try-and-catch block to handle the runtime exception in the *createProduct()* method.

    //ProductController.java
    @Transactional
    public void createProduct() {
        System.out.println("------ createProduct ------");
        Product prod = new Product();
        prod.setDescription("This is createProduct method.");
        prod.setPrice(10);
        prod.setTitle("Create Product");
        productRepository.save(prod);
        try{
            orderController.createOrder();
        }catch (RuntimeException e){
            System.out.println("Handle " + e.getMessage());
        }
    }

    //OrderController.java
    @Transactional
    public void createOrder() {
        System.out.println("------ createOrder ------");
        Order order = new Order();
        order.setTitle("Create Order");
        order.setDescription("This is createOrder method with runtime exception");
        orderRepository.save(order);  
        throw new RuntimeException("Create Order RuntimeException");
    }

Arg? Both records have been rollback. Why? At the same time, we also noticed the exception error.

    org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only

**@Transactional** has a parameter called **Propagation. **Propagation will define the transaction boundary. Spring will start and end the transaction according to the propagation setting. By default, propagation is set to **REQUIRED. **So, if there is an active transaction, Spring will consume it instead of creating a new transaction. Therefore, in the above scenario, even though we have caught the exception on the outer method, the transaction was still rolling back.

## Example 6

Use Propagation **REQUIRES_NEW, **this will force spring to create a subtransaction.

    @Transactional
    public void createProduct() {
        System.out.println("------ createProduct ------");
        Product prod = new Product();
        prod.setDescription("This is createProduct method.");
        prod.setPrice(10);
        prod.setTitle("Create Product");
        productRepository.save(prod);
        try{
            orderController.createOrder();
        }catch (RuntimeException e){
            System.out.println("Handle " + e.getMessage());
        }
    }

    @Transactional(propagation=Propagation.REQUIRES_NEW)
    public void createOrder() {
        System.out.println("------ createOrder ------");
        Order order = new Order();
        order.setTitle("Create Order");
        order.setDescription("This is createOrder method with runtime exception");
        orderRepository.save(order);  
        throw new RuntimeException("Create Order RuntimeException");
    }

![](https://cdn-images-1.medium.com/max/2956/1*leRcsxITEQaRcT7Y5UD8hA.png)

![](https://cdn-images-1.medium.com/max/2272/1*mDR4R3cfkwNK2cPjwNPNKQ.png)

The product record was written into the database, and the order record was rolled back.

## Example 7

What if we never use try and catch block in the outer method, and an exception has happened in the inner method? Will the outer transaction still commit?

    @Transactional
    public void createProduct() {
        System.out.println("------ createProduct ------");
        Product prod = new Product();
        prod.setDescription("This is createProduct method.");
        prod.setPrice(10);
        prod.setTitle("Create Product");
        productRepository.save(prod);
        orderController.createOrder();
    }

    @Transactional(propagation=Propagation.REQUIRES_NEW)
    public void createOrder() {
        System.out.println("------ createOrder ------");
        Order order = new Order();
        order.setTitle("Create Order");
        order.setDescription("This is createOrder method with runtime exception");
        orderRepository.save(order);  
        throw new RuntimeException("Create Order RuntimeException");
    }

We see that both transactions have been rolled back. Because the inner transaction threw an exception, the outer transaction detected the exception and it hasn’t been handled. Therefore, the outer transaction has been rolled back.

## **Example 8**

Let’s see if the exception has happened in the outer method instead.

    @Transactional
    public void createProduct() {
        System.out.println("------ createProduct ------");
        Product prod = new Product();
        prod.setDescription("This is createProduct method.");
        prod.setPrice(10);
        prod.setTitle("Create Product with runtime");
        productRepository.save(prod);
        orderController.createOrder();
        throw new RuntimeException("Create Product RuntimeException");
    }

    @Transactional(propagation=Propagation.REQUIRES_NEW)
    public void createOrder() {
        System.out.println("------ createOrder ------");
        Order order = new Order();
        order.setTitle("Create Order with propagation required_new");
        order.setDescription("This is createOrder method.");
        orderRepository.save(order);  
    }

![](https://cdn-images-1.medium.com/max/2968/1*s1qZ58JjHDRTD96sLqUlJw.png)

![](https://cdn-images-1.medium.com/max/2816/1*MiMYel38UTEyNHJkaq3tfA.png)

We see that the inner transaction has been committed successfully due to the **Propagation.REQUIRES_NEW**. This forces the Spring to handle the query in subtransaction. Once it is completed the outer transaction resumed, and an exception happened at this moment, so it rolled back.



## Additional Notes

To take note, if both methods are in the same class, the **@Transactional** annotation will not take place. It will always be treated as one transaction, so even if we specified the **Propagation.REQUIRES_NEW**, it won’t work. At the very beginning, we talked about Spring creating a proxy when seeing annotation @Transactional. When you are calling the internal method, it will bypass the proxy.

Thanks for reading!