Mastering Transaction Propagation in Spring Boot: Ensure Data Consistency and Integrity Across Database Operations

Sai Komal Pendela
Level Up Coding
Published in
8 min readApr 30, 2023

--

Photo by Towfiqu barbhuiya on Unsplash

When it comes to handling transactions in a Spring Boot application, one of the key aspects is propagation. Propagation refers to how transactions are handled in case of nested method calls, where a method is called from another method that is already running in a transactional context. In this article, we will explore the concept of propagation in transactions and how it can be used.

What is Transaction Propagation?

Transaction propagation is the ability to maintain the integrity of transactions when multiple transactions are running simultaneously. It defines how a transaction should behave when it is executed within the context of an existing transaction. In Spring Boot, transactions can be propagated using the @Transactional annotation, which specifies the propagation behavior.

Understanding the @Transactional annotation

The @Transactional annotation is used to define a method as a transactional operation. It can be used at both the class and method level. When used at the class level, it applies to all the methods in the class. When used at the method level, it applies only to that method. The @Transactional annotation takes several parameters, including propagation, isolation, timeout, and read-only.

Spring Boot provides the following propagation options:

REQUIRED(default)

PROPAGATION_REQUIRED is the default propagation behaviour of Spring transactions. When a method is called with PROPAGATION_REQUIRED, Spring checks if a transaction is already in progress. If there is no transaction in progress, a new transaction is started. If a transaction is already in progress, the current transaction is used.

In other words, PROPAGATION_REQUIRED ensures that a method call is executed within the scope of a transaction. If the caller of the method is already within a transaction, the called method will join the existing transaction. If the caller of the method is not within a transaction, a new transaction will be created for the called method.

Let’s explore the behaviour of PROPAGATION_REQUIRED using a practical example. Suppose we have a Spring Boot application that performs a transfer between two bank accounts. We have two methods, transferFunds and updateAccountBalance, which are both annotated with @Transactional(propagation = Propagation.REQUIRED).

@Service
@Transactional
class BankAccountService(val accountRepository: AccountRepository) {

fun transferFunds(fromAccountId: Long, toAccountId: Long, amount: BigDecimal) {
val fromAccount = accountRepository.findById(fromAccountId).orElseThrow()
val toAccount = accountRepository.findById(toAccountId).orElseThrow()
updateAccountBalance(fromAccount, -amount)
updateAccountBalance(toAccount, amount)
}

fun updateAccountBalance(account: Account, amount: BigDecimal) {
account.balance += amount
accountRepository.save(account)
}
}

In this example, the transferFunds method is the entry point of the transaction. The updateAccountBalance method is called twice within the transferFunds method, and it also has PROPAGATION_REQUIRED. When transferFunds is called, a new transaction is started, and all subsequent method calls within the transferFunds method are executed within this transaction.

If an exception is thrown within transferFunds, both updateAccountBalance calls will be rolled back, and the transfer will be canceled.

REQUIRES_NEW

PROPAGATION_REQUIRES_NEW creates a new transaction for the annotated method. If there is an existing transaction, it is suspended while the new transaction is created and committed. If the new transaction fails, it is rolled back, and the suspended transaction is resumed.

Let’s take a look at an example of how PROPAGATION_REQUIRES_NEW works. Suppose we have a service that performs two operations: one to save data to a database using Hibernate, and another to send a message to a RabbitMQ queue. We want to ensure that both operations are executed within their own transaction, such that if one operation fails, the other is not affected.

Here is an example implementation of this service:

@Service
class MyService(
private val repository: MyRepository,
private val rabbitTemplate: RabbitTemplate
) {

@Transactional(propagation = Propagation.REQUIRES_NEW)
fun saveToDatabase(myData: MyData) {
repository.save(myData)
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
fun sendToQueue(myData: MyData) {
rabbitTemplate.convertAndSend("my.queue", myData)
}

fun doBoth(myData: MyData) {
saveToDatabase(myData)
sendToQueue(myData)
}
}

In this example, we have two methods (saveToDatabase and sendToQueue) that are annotated with @Transactional(propagation = Propagation.REQUIRES_NEW). This means that each of these methods will create its own transaction and commit or roll back independently of the other.

The doBoth method calls both saveToDatabase and sendToQueue within its own non-transactional context. This means that if either method fails, the other will continue to execute independently. If an exception occurs during either saveToDatabase or sendToQueue, that method's transaction will be rolled back, but the other method's transaction will continue to execute.

SUPPORTS

PROPAGATION_SUPPORTS is used when you want to run a method in a transaction if one is available, but don’t create a new transaction if one doesn’t already exist. This is useful when you have a method that doesn’t modify any data but still needs to participate in a transaction if one is already active.

Let’s take an example where we have a service method that retrieves customer and order information. Both of these methods can be executed in the context of an existing transaction if one is available, but they don’t need to create a new transaction if one doesn’t already exist.

@Service
@Transactional
class CustomerService(private val customerRepository: CustomerRepository,
private val orderService: OrderService) {

@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
fun getCustomerById(id: Long): Customer? {
return customerRepository.findById(id).orElse(null)
}

@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
fun getOrdersByCustomerId(customerId: Long): List<Order> {
val customer = getCustomerById(customerId) ?: return emptyList()
return orderService.getOrdersByCustomer(customer)
}
}

In the above code, we have two service methods — getCustomerById and getOrdersByCustomerId — that are annotated with PROPAGATION_SUPPORTS. The getCustomerById method retrieves the customer information using the provided customer ID. If a transaction already exists, it participates in that transaction, but it doesn’t create a new transaction. The same goes for the getOrdersByCustomerId method, which retrieves the order information for a given customer. The readOnly attribute is set to true since the method only retrieves data and doesn't perform any updates.

NOT_SUPPORTED

PROPAGATION_NOT_SUPPORTEDspecifies that the current method should not be executed within a transaction context. This means that if a transaction is currently active, it will be suspended for the duration of the method execution, and any changes made within the method will not be included in the transaction.

Let’s take a closer look at how PROPAGATION_NOT_SUPPORTED works with an example. Suppose we have a service method that performs a non-transactional operation, such as logging an event to a database

@Service
class EventService(private val eventRepository: EventRepository) {

@Transactional(propagation = Propagation.NOT_SUPPORTED)
fun logEvent(event: Event) {
eventRepository.save(event)
}
}

In this example, we have annotated the logEvent method with @Transactional(propagation = Propagation.NOT_SUPPORTED), which tells Spring that this method should not participate in any existing transaction. Therefore, if another method calls logEvent while a transaction is active, the transaction will be temporarily suspended for the duration of the logEvent method call.

Let’s see an example of how this works in practice

@Service
class OrderService(
private val orderRepository: OrderRepository,
private val eventService: EventService
) {
@Transactional
fun createOrder(order: Order) {
orderRepository.save(order)
eventService.logEvent(Event("Order created"))
}
}

In this example, we have a service method createOrder that creates an order and logs an event using the logEvent method of EventService. The createOrder method is annotated with @Transactional, which means that any changes made to the database within this method should be committed as a single transaction.

However, since the logEvent method is annotated with @Transactional(propagation = Propagation.NOT_SUPPORTED), it will not participate in the transaction created by the createOrder method. Instead, it will execute as a separate, non-transactional operation.

This can be useful in scenarios where we want to perform an operation that does not require transactional consistency, such as logging or sending an email. By using PROPAGATION_NOT_SUPPORTED, we can execute such operations without impacting the transactional integrity of the rest of the application.

MANDATORY

PROPAGATION_MANDATORY specifies that a method must be executed within a transaction, otherwise an exception will be thrown. This is useful when you have a method that should always be called within the context of an existing transaction, and you want to ensure that it is not accidentally executed outside of that context.

Consider the following scenario: you have a service layer method that updates a user’s information in the database. This method should only be called when there is an existing transaction, as it needs to be executed atomically with other database operations.

Let’s look at an example implementation of this service method using PROPAGATION_MANDATORY

@Service
class UserService(private val userRepository: UserRepository) {

@Transactional(propagation = Propagation.MANDATORY)
fun updateUser(userId: Long, name: String, email: String) {
val user = userRepository.findById(userId)
.orElseThrow { EntityNotFoundException("User not found for id: $userId") }
user.name = name
user.email = email
userRepository.save(user)
}
}

In the above code, we have annotated the updateUser() method with @Transactional(propagation = Propagation.MANDATORY). This ensures that the method is executed within an existing transaction. If there is no existing transaction, a TransactionRequiredException will be thrown.

Let’s see how we can use this method:

@Service
class SomeOtherService(private val userService: UserService) {

@Transactional
fun someMethod() {
// Some database operations
userService.updateUser(1L, "John", "john@example.com")
// More database operations
}
}

In the code above, we have a service method someMethod() which is also annotated with @Transactional. This method calls the updateUser() method of the UserService. Since updateUser() is annotated with @Transactional(propagation = Propagation.MANDATORY), it can only be executed within an existing transaction. Therefore, someMethod() must also be annotated with @Transactional.

Now, if someMethod() is called without an existing transaction, a TransactionRequiredException will be thrown.

NEVER

PROPAGATION_NEVER specifies that a transaction should not be created if one does not already exist. If a transaction is already in progress when a method annotated with PROPAGATION_NEVER is called, an exception is thrown.

Let’s take a look at an example of using PROPAGATION_NEVER in a Spring Boot application:

@Service
class UserService(private val userRepository: UserRepository) {

@Transactional(propagation = Propagation.NEVER)
fun getUser(id: Long): User {
return userRepository.findById(id)
.orElseThrow { NotFoundException("User not found with ID $id") }
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
fun updateUser(user: User): User {
return userRepository.save(user)
}
}

In the above example, we have a UserService that uses a UserRepository to interact with the database. The getUser method is annotated with @Transactional(propagation = Propagation.NEVER), indicating that it should not participate in an existing transaction. If a transaction is already in progress when getUser is called, an exception will be thrown.

This can be useful in scenarios where we want to ensure that a specific operation is always performed outside of a transaction, or when we want to prevent unexpected side effects caused by nested transactions.

Conclusion

Transaction propagation is a powerful feature of Spring Boot that allows you to manage transactions in a multi-threaded environment. By understanding the different propagation options and how they work, you can ensure that your transactions are executed in the correct manner, and that your data remains consistent and accurate.

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job

--

--