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 theTransactionStatus
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.
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.
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.
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 theplaceOrder
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. TheplaceOrder
method still runs within its own transaction due toPropagation.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.
Starting a New Transaction: The method will always run within a new transaction, even if an existing transaction is active.
Suspending Existing Transaction: If an existing transaction is active, it will be suspended until the new transaction completes.
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.
Requiring an Active Transaction: The method must be called within an active transaction. If there is no active transaction, an exception is thrown.
Joining Existing Transaction: If an existing transaction is active, the method will join and run within that transaction.
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.
Prohibiting Active Transaction: The method must be called without an active transaction. If there is an active transaction, an exception is thrown.
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.
Suspending Active Transaction: If a transaction is active, it will be suspended, and the method will run without a transaction.
Immediate Commit: Any changes made within the method will be committed immediately, without waiting for the transaction to complete.
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
- 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 thecreateProductWithNotSupportedPropagationWithException()
method is called within this transaction, and since it's marked withPropagation.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:
Starting Outer Transaction: The test method is marked with
@Transactional
, so Spring starts a new transaction if one doesn't already exist.Calling Method with NOT_SUPPORTED: The
createProductWithNotSupportedPropagationWithException()
method is called. Since it's marked withPropagation.NOT_SUPPORTED
, it runs outside of the transaction. The outer transaction is suspended.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.
Throwing Exception: A
RuntimeException
is deliberately thrown after saving the product.No Rollback for Product: Since the method runs without a transaction, there’s nothing to roll back. The product record remains committed.
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.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.
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:
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.
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.
With Transaction:
Propagation.SUPPORTS
runs within the existing transaction, and an exception leads to a rollback of all changes.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 withPropagation.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 withPropagation.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.