Java Spring Boot Transaction Management

Java Spring Boot Transaction Management

Java Spring Boot Transaction Management

Introduction

In Spring applications, effective transaction management is essential to maintain data integrity during database operations. A transaction guarantees that a series of database actions either fully succeed or fail, adhering to the principles of atomicity, consistency, isolation, and durability (ACID). This comprehensive guide navigates through various transaction scenarios, behaviours, and best practices within Java Spring Transaction Management.

Spring Transaction Management

By default, Spring Boot operates in auto-commit mode for transactions, where each SQL statement is treated as its own transaction and is automatically committed. This example illustrates the default behaviour:

public void createProduct() {  
    Product prod = new Product();
    prod.setDescription("This is an example with runtime exception but no transactional.");
    prod.setPrice(10);
    prod.setTitle("First Product");
    productRepository.save(prod);
    throw new RuntimeException();
}

In this code, an exception is raised, yet it results in the successful insertion of the product into the database.

Screenshot-2023-10-30-at-3.32.33-PM Java Spring Boot Transaction Management

Example 1: Basic Usage of @Transactional

The @Transactional annotation in Spring Boot implicitly creates a proxy that initiates a transaction and commits it if no errors occur. When an exception arises, it ensures that the changes are rolled back. In this example, the transaction is rolled back when a RuntimeException occurs, maintaining data consistency:

@Transactional
public void createProductWithTransactional() {  
    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);
    throw new RuntimeException();
}
Screenshot-2023-10-30-at-3.32.33-PM Java Spring Boot Transaction Management

Handling Checked and Unchecked Exceptions

Spring Boot treats checked and unchecked exceptions differently by default. We can control this behavior using the rollbackFor and noRollbackFor attributes of the @Transactional annotation.

Example 2a: Checked Exception Without Rollback

In this example, the checked exception (e.g., SQLException) will not trigger a rollback because it’s a checked exception:

@Transactional
public void createProductWithCheckedException() throws Exception {  
    Product prod = new Product();
    prod.setDescription("This is an example with a checked exception and a transactional annotation.");
    prod.setPrice(10);
    prod.setTitle("Example 1a Product");
    productRepository.save(prod);
    throw new SQLException();
}
Screenshot-2023-10-30-at-3.39.17-PM Java Spring Boot Transaction Management

Example 2b: Using rollbackFor to Roll Back for Checked Exceptions

To roll back on checked exceptions, specify the rollbackFor attribute in the @Transactional annotation:

@Transactional(rollbackFor = SQLException.class)
public void createProductWithCheckedExceptionAndRollBackFor() throws Exception {  
    Product prod = new Product();
    prod.setDescription("This is an example with a checked exception and a transactional annotation with rollbackFor.");
    prod.setPrice(10);
    prod.setTitle("Example 2b Product");
    productRepository.save(prod);
    throw new SQLException();
}
Screenshot-2023-10-30-at-3.45.30-PM Java Spring Boot Transaction Management

Example 2c: Using noRollbackFor to Prevent Rollback for Specific Exceptions

Conversely, we may want to roll back for all exceptions except specific ones. Use the noRollbackFor attribute to achieve this:

@Transactional(noRollbackFor = RuntimeException.class)
public void createProductWithRuntimeExceptionAndNoRollBackFor() {
    Product prod = new Product();
    prod.setDescription(
      "This is an example with runtime exception, transactional annotation and noRollbackFor."
    );
    prod.setPrice(10);
    prod.setTitle("Example 2c Product");
    productRepository.save(prod);
    throw new RuntimeException();
}
Screenshot-2023-10-30-at-3.48.38-PM Java Spring Boot Transaction Management

Transactions and Try-Catch Blocks

This section explores how try-catch blocks interact with transactions in Spring Boot.

Example 3: Using Try-Catch Block Inside @Transactional

In this scenario, the try-catch block inside the method catches the RuntimeException. Since the exception is handled within the method, the transaction proceeds normally and gets committed:

@Transactional
public void createProduct() {
    try {
        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);
        throw new RuntimeException();
    } catch (Exception e) {
        System.out.println("Here we catch the exception.");
    }
}
Screenshot-2023-10-30-at-3.54.07-PM Java Spring Boot Transaction Management

Controlled Transactions with Propagation

Spring transactions allow us to control transaction behavior using propagation settings.

Example 4a: Transaction Behavior with Propagation.REQUIRED

In this example, both createProduct() and createOrder() methods are annotated with @Transactional. The createProduct() method calls the createOrder() method, and a RuntimeException is thrown inside createOrder().

// ProductService.java
@Transactional
public void createProductWithInnerCallingException() {
    Product prod = new Product();
    prod.setDescription(
      "This is an example runtime exception in inner method calling and transactional annotation."
    );
    prod.setPrice(10);
    prod.setTitle("Example 4a Product");
    productRepository.save(prod);
    this.orderService.createOrder();
}

// OrderService.java
@Transactional
public void createOrder() {
    Order order = new Order();
    order.setTitle("Example 4a Order");
    order.setDescription(
      "This is create order with runtime exception and transactional annotation"
    );
    orderRepository.save(order);
    throw new RuntimeException("Create Order RuntimeException");
}

In this example, both createProduct() and createOrder() transactions are rolled back if a runtime exception occurs in the createOrder() method. This is due to the default Propagation.REQUIRED behavior.

Screenshot-2023-10-30-at-4.02.15-PM Java Spring Boot Transaction Management
Screenshot-2023-10-30-at-4.02.56-PM Java Spring Boot Transaction Management

By default, the @Transactional annotation uses a propagation behavior called Propagation.REQUIRED. This means that if there is an active transaction, Spring will join that transaction instead of creating a new one.

In this example, when createOrder() is called from createProduct(), it joins the existing transaction started by createProduct().

Since a RuntimeException is thrown inside createOrder(), and it’s not caught within the method, the exception propagates to the calling method createProduct(). The exception is not handled by outer method as well, so the entire transaction is marked for rollback.

This ensures the atomicity of the transaction, meaning that all changes are either committed or rolled back together.

Example 4b: Transaction Behavior with Try-Catch in Propagation.REQUIRED

In this example, the createProduct() method calls the createOrder() method, where a RuntimeException is thrown. The createProduct() method catches this exception using a try-and-catch block.

// ProductService.java
@Transactional
public void createProductWithInnerCallingExceptionAndTryCatch() {
    try {
      Product prod = new Product();
      prod.setDescription(
        "This is an example runtime exception in inner method calling and transactional annotation."
      );
      prod.setPrice(10);
      prod.setTitle("Example 4b Product");
      productRepository.save(prod);
      this.orderService.createOrder();
    } catch (Exception e) {
      System.out.println("Here we catch the runtime exception.");
    }
}

// OrderService.java
@Transactional
public void createOrder() {
    Order order = new Order();
    order.setTitle("Example 4b Order");
    order.setDescription(
      "This is create order with runtime exception and transactional annotation"
    );
    orderRepository.save(order);
    throw new RuntimeException("Create Order RuntimeException");
}

In this case, even though the exception is caught, both transactions are rolled back because the transaction is already marked for rollback before the catch block is executed.

Screenshot-2023-10-30-at-4.06.01-PM Java Spring Boot Transaction Management
org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only

The exception org.springframework.transaction.UnexpectedRollbackException indicates that the transaction was silently rolled back because it was marked as rollback-only. The behaviour ensures the atomicity of the transaction. If one part of the transaction fails, the entire transaction is rolled back, maintaining the consistency of the system.

Example 4c: Transaction Behavior with Try-Catch in Propagation.NEW

Using Propagation.REQUIRES_NEW forces Spring to create a subtransaction, allowing the outer transaction to commit even if the inner transaction rolls back. In this example, the product record is written into the database, and the order record is rolled back:

@Transactional
public void createProductWithInnerCallingExceptionTryCatchAndPropagationNew() {
    try {
      Product prod = new Product();
      prod.setDescription(
        "This is an example runtime exception and propagation NEW in inner method calling and transactional annotation."
      );
      prod.setPrice(10);
      prod.setTitle("Example 4c Product");
      productRepository.save(prod);
      this.orderService.createOrderWithPropagationNEW();
    } catch (Exception e) {
      System.out.println("Here we catch the runtime exception.");
    }
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrderWithPropagationNEW() {
    Order order = new Order();
    order.setTitle("Example 4c Order");
    order.setDescription(
      "This is create order with runtime exception, transactional annotation and propagation NEW"
    );
    orderRepository.save(order);
    throw new RuntimeException("Create Order RuntimeException");
}
Screenshot-2023-10-30-at-4.10.48-PM Java Spring Boot Transaction Management
Screenshot-2023-10-30-at-4.10.57-PM Java Spring Boot Transaction Management

Example 4d: Transaction Behavior without Try-Catch in Propagation.NEW

When an exception occurs in the inner method createOrder(), and it’s not caught, both transactions are rolled back. The inner transaction marked for rollback also affects the outer transaction.

@Transactional
public void createProductWithInnerCallingExceptionPropagationNew() {
    Product prod = new Product();
    prod.setDescription(
        "This is an example runtime exception and propagation NEW in inner method calling, outer method no try catch and transactional annotation."
    );
    prod.setPrice(10);
    prod.setTitle("Example 4d Product");
    productRepository.save(prod);
    this.orderService.createOrderWithExceptionPropagationNEW();
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrderWithExceptionPropagationNEW() {
    Order order = new Order();
    order.setTitle("Example 4d Order");
    order.setDescription(
      "This is create order with runtime exception, transactional annotation and propagation NEW"
    );
    orderRepository.save(order);
    throw new RuntimeException("Create Order RuntimeException");
}

Example 4e: Transaction Behavior with Exception in Outer Method for Propagation.NEW

In this case, the exception is thrown in the outer method createProduct() after the inner method createOrder() has completed successfully. This results in the outer transaction being rolled back, but the inner transaction is not affected.

@Transactional
public void createProductWithInnerCallingPropagationNewOuterException() {
    Product prod = new Product();
    prod.setDescription(
      "This is an example with transactional annotation, outer method exception and inner calling with propagation NEW."
    );
    prod.setPrice(10);
    prod.setTitle("Example 4e Product");
    productRepository.save(prod);
    this.orderService.createOrderPropagationNEWWithoutException();
    throw new RuntimeException("Create Product RuntimeException");
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrderPropagationNEWWithoutException() {
    Order order = new Order();
    order.setTitle("Example 4e Order");
    order.setDescription(
      "This is create order with transactional annotation and propagation NEW"
    );
    orderRepository.save(order);
}

It’s essential to note that if both methods are in the same class, the @Transactional annotation will not create a new transaction, even with Propagation.REQUIRES_NEW. This is because the internal method call will bypass the proxy created by Spring, and the propagation setting will not take effect.

Screenshot-2023-10-30-at-4.22.42-PM Java Spring Boot Transaction Management
Screenshot-2023-10-30-at-4.23.22-PM Java Spring Boot Transaction Management

Notes: However, it’s essential to note that if both methods are in the same class, the @Transactionalannotation will not create a new transaction, even with Propagation.REQUIRES_NEW. This is because the internal method call will bypass the proxy created by Spring, and the propagation setting will not take effect.

Conclusion

In this tutorial, we explored the fundamental concepts of transactions in Spring and how to handle exceptions within these transactions. We began by understanding the basic usage of the @Transactional annotation, which automatically starts and commits transactions, rolling them back when exceptions occur. We dive into the differentiation between checked and unchecked exceptions, exploring how to control rollback behavior using attributes like rollbackFor and noRollbackFor.

In the later part, we examined the interaction between transactions and try-catch blocks. Furthermore, we explored the use of Propagation.REQUIRES_NEW to create new transactions and how the behavior of inner and outer transactions can be controlled independently.

Share this content:

Leave a Comment

Discover more from nnyw@tech

Subscribe now to keep reading and get access to the full archive.

Continue reading