Spring Transaction Propagation with Code Explanation

Spring Transaction Propagation with Code Explanation

·

14 min read

In Spring, a transaction represents a single unit of work that must be performed atomically. Transactions ensure data consistency and integrity by providing the ability to undo changes if an error occurs. Spring uses a transaction manager to manage transactions, and it supports various propagation behaviors to control how transactions are handled.

What is Propagation?

Propagation in Spring transactions determines how transactions should behave when they are nested within other transactions. Here are the available propagation options:

1. REQUIRED

  • Default option.

  • If a transaction already exists, the method will execute within that transaction.

  • If no transaction exists, a new one will be started.

  • Example: Useful for typical read-write operations where atomicity is required.

2. REQUIRES_NEW

  • Always starts a new transaction, even if one already exists.

  • Suspends the existing transaction until the new one is complete.

  • Example: Useful when you want to ensure that a specific operation is always executed in a new transaction.

3. SUPPORTS

  • Participates in a transaction if one already exists.

  • Doesn’t start a new transaction if one doesn’t exist.

  • Example: Useful for read-only operations that can be part of a transaction but don’t necessarily need to be.

4. MANDATORY

  • Requires that a transaction already exists.

  • If a transaction doesn’t exist, an exception is thrown.

  • Example: Useful when you want to enforce that a method must be called within an existing transaction.

5. NOT_SUPPORTED

  • Specifies that a method should not participate in a transaction at all.

  • Suspends any existing transaction.

  • Example: Useful for non-transactional operations within a transactional context.

6. NEVER

  • Specifies that a method should never be called within a transaction.

  • If a transaction is active, an exception is thrown.

  • Example: Useful when you want to enforce that a method must not be called within a transaction.

7. SUPPORTS

  • If a transaction is active, the method will run within it.

  • If a transaction is not active, the method will run outside of the transaction.

  • Example: Useful for operations that can be part of a transaction but can also run outside of one.

Automatic and Manual Rollback

Spring allows both automatic and manual rollback:

  • Automatic Rollback: This occurs when a runtime exception is thrown from a transactional method.

  • Manual Rollback: Can be performed by calling the setRollbackOnly() method on the TransactionStatus object.

Examples and Use Cases

Below are some code examples and use cases that demonstrate how different propagation behaviors work in practice.

Propagation.REQUIRED

Propagation.REQUIRED is the default propagation behavior in Spring transactions. It means that if a transaction is already active when a method annotated with @Transactional(propagation = Propagation.REQUIRED) is called, the method will execute within that existing transaction. If no transaction exists, a new one will be started.

  1. Within an Existing Transaction: If the method is called within an existing transaction, it joins that transaction, and all operations are part of the same transactional context.

  2. Without an Existing Transaction: If the method is called outside of a transaction, a new transaction is started, and all operations within the method are part of this new transactional context.

  3. Exception Handling: If an exception is thrown within the method and is not caught, the transaction will be rolled back, and all changes made within the transaction will be undone.

Let’s consider an example where we have a method placeOrder that saves an order and its associated order items to the database.

/**
* OrderService.java
*/
@Transactional(propagation = Propagation.REQUIRED)
public void placeOrder(Order order) {
    // Save the order
    orderRepository.save(order);

    for (OrderItem item : order.getItems()) {
        // Find the product
        Product product = productRepository.findById(item.getProduct().getId())
                .orElseThrow(() -> new RuntimeException("Product not found"));

        // Update the stock quantity
        int newQuantity = product.getQuantity() - item.getQuantity();
        if (newQuantity < 0) {
            throw new RuntimeException("Insufficient stock");
        }
        product.setQuantity(newQuantity);
        productRepository.save(product);

        // Save the order item
        item.setOrder(order);
        orderItemRepository.save(item);
    }

    order.setStatus(OrderStatus.COMPLETED);
    orderRepository.save(order);
}

@Test(expected = RuntimeException.class)
public void testPlaceOrderWithRequiredPropagationAndException() {
    Order order = new Order();
    order.setCustomerName("testPlaceOrderWithRequiredPropagationAndException");
    order.setStatus(OrderStatus.PENDING);

    List<OrderItem> items = new ArrayList<>();
    // Add order items
    OrderItem item1 = new OrderItem();
    Product product1 = new Product();
    product1.setName("Product 1 created in testPlaceOrderWithRequiredPropagationAndException");
    product1.setPrice(BigDecimal.valueOf(10.0));
    product1.setQuantity(5);
    productRepository.save(product1);

    item1.setOrder(order);
    item1.setProduct(product1);
    item1.setQuantity(2);
    items.add(item1);

    OrderItem item2 = new OrderItem();
    Product product2 = new Product();
    product2.setName("Product 2 created in testPlaceOrderWithRequiredPropagationAndException");
    product2.setPrice(BigDecimal.valueOf(20.0));
    product2.setQuantity(10);
    productRepository.save(product2);
    item2.setOrder(order);
    item2.setProduct(product2);
    item2.setQuantity(15); // Here will cause exception because quantity greater than stock.
    items.add(item2);

    order.setItems(items);
    orderService.placeOrder(order);

    // Verify that the order was not saved
    Order savedOrder = orderRepository.findById(order.getId()).orElse(null);
    assertNull(savedOrder);

    // Verify that the product 1 quantities were not updated
    Product updatedProduct1 = productRepository.findById(product1.getId()).orElse(null);
    assertNotNull(updatedProduct1);
    assertEquals(5, updatedProduct1.getQuantity());

    // Verify that the product 2 quantities were not updated
    Product updatedProduct2 = productRepository.findById(product2.getId()).orElse(null);
    assertNotNull(updatedProduct2);
    assertEquals(10, updatedProduct2.getQuantity());
}

Test Scenario

In the test method, we call the placeOrder method with an order item that has a quantity greater than the available stock, causing an exception to be thrown.

  • Result: Both the order and its order items will be rolled back, and no data will be persisted in the database.

  • Reason: The exception triggers a rollback of the transaction, and since the method is running with Propagation.REQUIRED, all changes made within the method are part of the same transactional context and are rolled back.

Notes

  • If the test method itself is marked with @Transactional, the entire test runs within a transaction, including the setup (e.g., saving product records). If an exception occurs within the placeOrder method, the entire test transaction, including the setup, would be rolled back.

  • If the test method is not marked with @Transactional, the setup runs outside of a transaction, and any changes made during the setup are committed immediately. The placeOrder method still runs within its own transaction due to Propagation.REQUIRED.

Propagation.REQUIRES_NEW

Propagation.REQUIRES_NEW is a propagation behavior in Spring transactions that ensures a new transaction is always started when a method is called, regardless of whether an existing transaction is already active. If there is an active transaction, it will be suspended, and a new one will be started.

  1. Starting a New Transaction: The method will always run within a new transaction, even if an existing transaction is active.

  2. Suspending Existing Transaction: If an existing transaction is active, it will be suspended until the new transaction completes.

  3. Exception Handling: If an exception is thrown within the method, only the changes made within the current transaction (the new one) will be rolled back. The outer transaction (if any) will continue unaffected.

Let’s consider an example where you have a service method that updates the product quantity. Using REQUIRES_NEW, the product quantity update will run in a separate transaction, and any exception that occurs in this transaction will not affect the outer transaction.

/**
* ProductService.java
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateProductQuantityWithRequiresNewPropagation(OrderItem item) {
    // Find the product
    Product product = productRepository.findById(item.getProduct().getId())
            .orElseThrow(() -> new RuntimeException("Product not found"));

    // Update the stock quantity
    int newQuantity = product.getQuantity() - item.getQuantity();
    if (newQuantity < 0) {
        throw new RuntimeException("Insufficient stock");
    }
    product.setQuantity(newQuantity);
    productRepository.save(product);
}

/**
* SpringTransactionTest.java
* Description.    : `updateProductQuantityWithRequiresNewPropagation` was using REQUIRES_NEW propagation, 
*                   if exception happened every action inside this transaction should rollback, but the 
*                   outer transaction should continue without affected.
* Expected Result : Order record is created, product record is rollback.
*/
@Test
@Transactional
public void testUpdateProductQuantityWithRequiresNewPropagationAndInnerException() {
    // Create one order
    Order order = new Order();
    order.setCustomerName("testUpdateProductQuantityWithRequiresNewPropagationAndInnerException");
    order.setStatus(OrderStatus.PENDING);
    orderRepository.save(order);

    try {
        OrderItem item = new OrderItem();
        item.setProduct(new Product()); // Here we passed empty Product, so exception occured.
        productService.updateProductQuantityWithRequiresNewPropagation(item);
    } catch (RuntimeException e) {

    }

   // Verify that the order was created.
   Order savedOrder = orderRepository.findById(order.getId()).get();
   assertEquals(order.getCustomerName(), savedOrder.getCustomerName());
}

Test Scenario

In the test method, we create an order and then call the updateProductQuantityWithRequiresNewPropagation method, passing an empty product, causing an exception.

  • Result: The order record is created, but the product record is rolled back.

  • Reason: The method runs with Propagation.REQUIRES_NEW, so the exception only affects the inner transaction (product update), and the outer transaction (order creation) continues without being affected.

Notes

  • If you don’t catch the exception (using a try-catch block), any exception thrown by the updateProductQuantityWithRequiresNewPropagation method will propagate to the caller, and the entire outer transaction will be rolled back.

  • The use of Propagation.REQUIRES_NEW ensures that the inner transaction is completely isolated from the outer transaction. This isolation can be useful in scenarios where you want to ensure that a specific operation does not affect the overall transactional flow.

Propagation.MANDATORY

Propagation.MANDATORY is a propagation behavior in Spring transactions that requires an existing transaction to be active when a method is called. If no transaction is active, an IllegalTransactionStateException will be thrown. It ensures that specific operations are always executed within a transactional context.

  1. Requiring an Active Transaction: The method must be called within an active transaction. If there is no active transaction, an exception is thrown.

  2. Joining Existing Transaction: If an existing transaction is active, the method will join and run within that transaction.

  3. Exception Handling: If no transaction is active, an IllegalTransactionStateException is thrown, indicating that the method requires a transaction but none was found.

Let’s consider an example where you have an updateOrderStatus method that updates an order status in the database. If the method is called outside of a transaction, an exception will be thrown, since it requires a transaction to be active.

/**
* OrderService.java
*/
@Transactional(propagation = Propagation.MANDATORY)
public void updateOrderStatus(Long orderId, OrderStatus status) {
    Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new RuntimeException("Order not found"));
    order.setStatus(status);
    orderRepository.save(order);
}

/**
* SpringTransactionTest.java
* Description.    : `updateOrderStatus` was using MANDATORY propagation, 
*                   if the caller method doesn't initiate a transaction, 
*                   an exception will throw.
* Expected Result : IllegalTransactionStateException occured.
*/
@Test
public void testMandatoryPropagationWithoutTransaction() {
    Order order = new Order();
    order.setCustomerName("testMandatoryPropagationWithoutTransaction");
    order.setStatus(OrderStatus.PENDING);
    orderRepository.save(order);
    assertThrows(IllegalTransactionStateException.class, () -> {
            orderService.updateOrderStatus(order.getId(), OrderStatus.COMPLETED);
    });
}

Test Scenario

In the test method, we create an order and then call the updateOrderStatus method without initiating a transaction.

  • Result: IllegalTransactionStateException is thrown.

  • Reason: The method runs with Propagation.MANDATORY, so it requires an active transaction. Since no transaction was initiated, an exception is thrown.

Propagation.NEVER

Propagation.NEVER is a propagation behavior in Spring transactions that specifies that a transaction must not be active when a method is called. If a transaction is already active, an IllegalTransactionStateException will be thrown.

  1. Prohibiting Active Transaction: The method must be called without an active transaction. If there is an active transaction, an exception is thrown.

  2. Exception Handling: If a transaction is active, an IllegalTransactionStateException is thrown, indicating that the method must not be run within a transaction.

Let’s consider an example where you have a deleteOrder method that deletes an order and its details from the database. If the method is called within a transaction, an exception will be thrown, since it should not be run within a transaction.

@Transactional(propagation = Propagation.NEVER)
public void deleteOrder(Order order) {
    this.orderRepository.delete(order);
    this.orderDetailRepository.delete(order.getDetail());
}

@Test
@Transactional
public void testNeverPropagationWithTransaction() {
    assertThrows(IllegalTransactionStateException.class, () -> {
        orderService.deleteOrder(new Order());
    });
}

Test Scenario

In the test method, we attempt to delete an order within a transaction.

  • Result: IllegalTransactionStateException is thrown.

  • Reason: The method runs with Propagation.NEVER, so it must not be run within an active transaction. Since the test method is annotated with @Transactional, an exception is thrown.

Propagation.NOT_SUPPORTED

Propagation.NOT_SUPPORTED is a propagation behavior in Spring that specifies that the method should not run within a transaction. If a transaction is already active, it will be suspended until the method is complete. Any changes made to the database within the method will be committed immediately, rather than at the end of the transaction.

  1. Suspending Active Transaction: If a transaction is active, it will be suspended, and the method will run without a transaction.

  2. Immediate Commit: Any changes made within the method will be committed immediately, without waiting for the transaction to complete.

  3. Exception Handling: If an exception occurs within the method, it does not affect the transaction, as the method is run without a transaction.

Let’s consider an example where you have a method to create a product. This method is designed to run without a transaction, even if a transaction is active.

/**
* ProductService.java
*/
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void createProductWithNotSupportedPropagationWithException() {
    Product product = new Product();
    product.setName("This product created with NOT_SUPPORTED propagation and exception.");
    product.setQuantity(10);
    product.setPrice(BigDecimal.valueOf(10.0));
    productRepository.save(product);
    throw new RuntimeException("DummyException: Simulating an error");
}

/**
* SpringTransactionTest.java
* Expected Result : product was created, order was rollback.
*/
@Test(expected = RuntimeException.class)
@Transactional
public void testCreateProductWithNotSupportedPropagationWithTransactionAndException() {

    Order order = new Order();
    order.setCustomerName("testCreateProductWithNotSupportedPropagationWithTransactionAndException");
    order.setStatus(OrderStatus.PENDING);
    orderRepository.save(order);

    productService.createProductWithNotSupportedPropagationWithException();
}

/**
* SpringTransactionTest.java
* Expected Result : product was created, order was created.
*/
@Test(expected = RuntimeException.class)
public void testCreateProductWithNotSupportedPropagationWithoutTransactionAndException() {
    Order order = new Order();
    order.setCustomerName("testCreateProductWithNotSupportedPropagationWithoutTransactionAndException");
    order.setStatus(OrderStatus.PENDING);
    orderRepository.save(order);

    productService.createProductWithNotSupportedPropagationWithException();
}

Test Scenarios

  1. In the first test case, the method is marked with @Transactional, meaning Spring will establish a new transaction if one does not already exist. When the createProductWithNotSupportedPropagationWithException() method is called within this transaction, and since it's marked with Propagation.NOT_SUPPORTED, it will run non-transactionally. The outer transaction that was active will be suspended for the duration of this method.

Here’s a step-by-step breakdown of what happens:

  1. Starting Outer Transaction: The test method is marked with @Transactional, so Spring starts a new transaction if one doesn't already exist.

  2. Calling Method with NOT_SUPPORTED: The createProductWithNotSupportedPropagationWithException() method is called. Since it's marked with Propagation.NOT_SUPPORTED, it runs outside of the transaction. The outer transaction is suspended.

  3. Saving Product: Within this method, a product is saved to the database. Since there’s no transaction in this method, the save operation is committed immediately.

  4. Throwing Exception: A RuntimeException is deliberately thrown after saving the product.

  5. No Rollback for Product: Since the method runs without a transaction, there’s nothing to roll back. The product record remains committed.

  6. Handling Exception in Outer Transaction: The RuntimeException thrown from the inner method propagates to the outer method. Since it's not handled properly and the outer method is transactional, the outer transaction is rolled back.

  7. Rollback of Order Record: Any changes made within the outer transaction, such as the creation of an order record, are rolled back due to the exception.

  8. Resuming Outer Transaction: Once the inner method is complete, the outer transaction is resumed and subsequently rolled back due to the exception.

Let’s see the second test case:

  1. Without Transaction: If the method is called without a transaction, the product is created, and the exception is thrown. Both the product and order records are committed, as there is no transaction to roll back.

  2. Because the test method doesn’t annotate with @Transactional, so even though the RuntimeException was propagated to the outer method, with nothing to rollback so both the product and order records were committed.

Propagation.SUPPORTS:

Propagation.SUPPORTS is a transaction propagation setting that allows a method to run within an existing transaction if one is already active. If no transaction is active, the method will run non-transactionally.

  1. With Transaction: Propagation.SUPPORTS runs within the existing transaction, and an exception leads to a rollback of all changes.

  2. Without Transaction: Propagation.SUPPORTS runs non-transactionally, and an exception does not affect the committed changes.

Suppose you have a service method that creates a product in the database. The behavior of this method depends on whether it is called within an active transaction or outside of a transaction.

/**
* ProductService.java
*/
@Transactional(propagation = Propagation.SUPPORTS)
public Product createProductWithSupportsPropagationAndException() {
    Product product = new Product();
    product.setName("This product created with SUPPORTS propagation and exception.");
    product.setQuantity(10);
    product.setPrice(BigDecimal.valueOf(10.0));
    productRepository.save(product);
    throw new RuntimeException("DummyException: Simulating an error");
}

/**
* SpringTransactionTest.java
* Expected Result : product was rollback, order was rollback.
*/
@Test(expected = RuntimeException.class)
@Transactional
public void testCreateProductWithSupportPropagationWithTransactionAndException() {
    Order order = new Order();
    order.setCustomerName("testCreateProductWithSupportPropagationWithTransactionAndException");
    order.setStatus(OrderStatus.PENDING);
    orderRepository.save(order);

    productService.createProductWithSupportsPropagationAndException();
}

/**
* SpringTransactionTest.java
* Expected Result : product was created, order was created.
*/
@Test(expected = RuntimeException.class)
public void testCreateProductWithSupportPropagationWithoutTransactionAndException() {
    Order order = new Order();
    order.setCustomerName("testCreateProductWithSupportPropagationWithoutTransactionAndException");
    order.setStatus(OrderStatus.PENDING);
    orderRepository.save(order);

    productService.createProductWithSupportsPropagationAndException();
}

Test Case 1: With Transaction

  • Marking Test as Transactional: The test method is marked with @Transactional, so Spring will start a new transaction if one doesn't already exist.

  • Calling Method with SUPPORTS: The createProductWithSupportsPropagationAndException() method is called within this transaction. Since it's marked with Propagation.SUPPORTS, it runs within the existing transaction.

  • Saving Product and Throwing Exception: A product is saved to the database, and then a RuntimeException is deliberately thrown.

  • Rollback of Product and Order: Since both the inner and outer methods are part of the same transaction, and an exception is thrown, all changes are rolled back. Both the product and order records are rolled back.

Test Case 2: Without Transaction

  • No Transactional Annotation: The test method is not marked with @Transactional, so no transaction is started.

  • Calling Method with SUPPORTS: The createProductWithSupportsPropagationAndException() method is called without an active transaction. Since it's marked with Propagation.SUPPORTS, it runs non-transactionally.

  • Saving Product and Throwing Exception: A product is saved to the database, and then a RuntimeException is deliberately thrown.

  • Commit of Product and Order: Since no transaction is active, there’s nothing to roll back. Both the product and order records are committed, even though an exception is thrown.

Additional Notes

There is one more propagation Propagation.NESTED not covered in this tutorial, because the NESTED propagation behavior is not supported by all transaction managers in Spring.

In Hibernate with JPA, the NESTED propagation level is not supported out of the box. The JPA specification itself does not define anything equivalent to the nested transaction behavior that you might find in some other transaction management systems.

When using JpaTransactionManager in Spring with Hibernate, if you try to set the propagation level to NESTED, you'll receive an exception, as this behavior is not supported.

NESTED starts a "nested" transaction if a transaction is already in progress. If the nested transaction fails, it will be rolled back without affecting the outer transaction. If no transaction is active, it behaves like REQUIRED.

// ProductService.java
@Transactional(propagation = Propagation.NESTED)
public void createProductWithNestedPropagation() {
    // Implementation here
}

// SpringTransactionTest.java
@Test
@Transactional
public void testCreateProductWithNestedPropagation() {
    // Test code here
}

Explanation:

  • With Transaction: If called within an active transaction, a nested transaction is started. If the nested transaction fails, it can be rolled back without affecting the outer transaction.

  • Without Transaction: If called without an active transaction, it behaves like REQUIRED, starting a new transaction.

Conclusion

Understanding Spring’s transaction propagation options is essential for managing transactions effectively in a Spring application. By choosing the right propagation behavior, developers can control how transactions are handled, ensuring data consistency and integrity. 🌱

Leave a comment below if you need a full source code.

Did you find this article valuable?

Support Wynn’s blog by becoming a sponsor. Any amount is appreciated!