Simplifying Transaction Management in Spring Boot: A Practical Guide

Konstantin Borimechkov
7 min readJul 7, 2023

--

In this article, we will explore the complex subject of transaction management in API engineering.

If you are reading this, you have most probably worked with transactions, so there is no need for me to explain the difficulties that they bring. There are a lot of trade-offs to consider and with that, transaction management becomes a 🔑 concept to learn for every engineer, in my opinion! ✌️

What is a transaction in the first place? 🤔

A transaction refers to a logical unit of work that encompasses multiple database operations. It is a way to group related database operations together and ensure that they are executed atomically, meaning either all of them succeed or none of them take effect.

Not gonna dive into the specifics of what’s a transaction and it’s attributes, but this article provides very detailed and well-structured explanation on the topic!

To sum it up, a transaction:

  • needs to be ACID
  • has different DB access operations (READ, WRITE, COMMIT)
  • has transaction states, which tell about the current state of the Transaction and also tell how we will further do the processing in the transactions

Why is Transaction Management so important?

Lets have a look at the bigger pinpoints, answering this question 👇

Data Consistency & Reliability

Transaction management ensures atomicity, by ensuring that a set of related database operations either succeeds or fails as a single unit. This prevents situations where only some parts of an operation are completed, leaving the system in an inconsistent state.

By ensuring the reliable execution of operations, transaction management helps maintain the overall integrity of the application.

Performance Optimization

There are multiple ways in which the transaction management, provided by Spring Boot (and most probably other frameworks), helps developers in writing performance optimized APIs! Some of which are:

  1. Optimized Resource Utilization
  • Using transactions enables us the ability to acquire and release database connections efficiently, by acquiring a database connection and holding it until the transaction completes. Minimizing the overhead of establishing a new connection for each database operation!

2.`Reduction in DB Round Trips

  • When dealing with frequent database interactions, fewer round trips between the application and database, minimize the network latency and overhead. This reduction in round trips is done by the transactional management, which allows for grouping multiple DB operations into a single unit!

3. Read Consistencies and Caching

  • TM allows us to leverage caching mechanisms to store frequently accessed data in memory. By caching commonly accessed data within a transaction, READ operations can be served directly from the cache instead of each time going to the database itself. This results in much faster response times for the end-users! ⏩

Error Handling and Recovery

The biggest benefit by far here is that transaction management enables for: rollback on exception. Meaning, if an unchecked exception is thrown within a transactional method, Spring Boot’s transaction management automatically roll backs the transaction. This ensures that any changes made within the transaction are undone, preserving data consistency. When it comes to checked exceptions, they don’t trigger rollbacks unless explicitly configured.

Other benefits include:

  • the ability to customize roll back rules
  • declarative error handling with the use of aspects
  • enabling you to separate error handling concerns from the business logic
  • retrievable mechanisms
  • compensation transactions (via Spring SAGA for example)

Transaction Management types

In Spring Boot there are two types of transaction management: Declarative & Programmatic. Both serve their purpose, depending on the trade-offs you’re willing to take. Here is a quick breakthrough of each approach:

Declarative Transaction Management

Being one of the most widely used ways of managing transactions in Spring Boot, this approach can be achieved either by our favourite XML configurations 👿 or by annotations 😍.

The main goal of this approach is to let you focus on implementing the business logic, without explicit transaction management code!

Focusing on the annotations approach, here comes the familiar to most, @Transactional annotation, for which I’ve written an article, where you’ll find a lot more details on this annotation!

👨‍💻 Quick code example of this approach:

@Service
@RequiredArgsConstructor
public class PaymentServiceImpl implements PaymentService {

private final WalletRepository walletRepository;
private final PaymentGateway paymentGateway;

@Override
@Transactional
public void processPayment(String userId, double amount) {
walletTransaction(userId, amount);
externalApiTransaction(userId, amount);
}
}

.. and just for reference, this is what the @Transactional annotation does under the hood in the processPayment method:

@Override
public void processPayment(String userId, double amount) {
Connection connection = dataSource.getConnection();
try (connection) {
connection.setAutoCommit(false);

// execute business logic

connection.commit();
} catch (SQLException e) {
connection.rollback();
}
}

With that said, here are a few pinpoints on the benefits that declarative TM brings:

  • By applying @Transactional to a method or class, you can specify the transactional behaviour for that method or all methods within the class.
  • The flexibility to specify different aspects of the transactions, This includes:
  1. setting the propagation behaviour
  2. configuring the isolation levels
  3. configuring the timeout durations
  4. making transactions read-only, which improves performance by optimizing DB resources via read-only mechanisms, reduced amount of DB locking and lowering the chance of concurrency issues!
  • The ability to leverage Spring Boot’s proxy-based mechanisms to manage transactions declaratively. Meaning that when starting the application, if Spring detects a bean annotated with @Transactional , it creates a dynamic proxy of that bean! This proxy will have access to a TransactionManager, who uses simply JDBC connection. The proxy will then ask the TM to open/close transactions.
  • Implemented roll back mechanism to roll back a transaction if a RuntimeException.class or error occurs (unchecked exception).

❗️Checked exceptions (Exception.class) aren’t automatically rolled back and for them, we can use the@Transactional attribute rollbackFor, which can be used for all types of exceptions, but it’s good practise to use it only for specifying exact checked exception/s that you want to be rolled back.

By defining it like so 👇

@Override
@Transactional(rollbackFor=SpecificCheckedException.class)
public void processPayment(String userId, double amount) {...}

you are telling the TM to roll back the transaction in case this checked exception occurs!

Programatic Transaction Management

Although harder to use, this approach provides us with a fine-grained control over our transactions by enabling us to manage them with code!

With programatic TM, in stead of using annotations, we use code to control define when and how transactions should be initiated, committed, or rolled back.

When to consider this approach over the declarative one? 🤔

  • if you are dealing with complex transactional scenarios and a more fine-grained control over the transactional boundaries would be of help
  • when dealing with DB and external API calls within a single transaction, the flexibility and control you get from this approach could be very beneficial and should be considered, IMO ✌️

Since we don’t have the ‘easy’ annotations, how can we implement this approach in our code? 🤨

Here comes the TransactionTemplate class in Spring Boot, which encapsulates the necessary boilerplate code for managing transactions!

🧐 Let’s go trough a little code, to see how can we put this into practise and leverage it’s benefits (I will add some comments in the code, so make sure to follow-trough):

@Service
@RequiredArgsConstructor
public class PaymentServiceImpl implements PaymentService {

private final WalletRepository walletRepository;
private final PaymentGateway paymentGateway;
private final TransactionTemplate transactionTemplate;

// The main method for proccessing a payment,
// which is then called from the API layer
@Override
public void processPayment(String userId, double amount) {

transactionTemplate.execute(status -> {
// Update the wallet balance within its transaction
walletTransaction(userId, amount);
// Call the external API within a separate transaction
externalApiTransaction(userId, amount);
return null;
});
}

// This method uses the transactionTemplate to encapsulates
// all walletRepository calls within a single/separate transaction
private void walletTransaction(String userId, double amount) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
Wallet wallet = walletRepository.findByUserId(userId);
if (wallet.getBalance() >= amount) {
wallet.setBalance(wallet.getBalance() - amount);
walletRepository.save(wallet);
} else {
throw new InsufficientBalanceException("Insufficient balance in the wallet.");
}
}
});
}

// This method uses the transactionTemplate to encapsulate
// the external paymentGateway API call within a single/separate transaction
private void externalApiTransaction(String userId, double amount) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
paymentGateway.processPayment(userId, amount);
}
});
}
}

As you can see, we have a lot more code than simply using annotations, but this gives us more flexibility in drawing the control of transactional boundaries. By using the TransactionTemplate, you can achieve separate transactions for different parts of the payment process. This allows for better control over the transactional behaviour and isolation of the wallet update and the external API call.

✍️ In conclusion, both approaches have their pros and cons, so like with everything in software engineering, it’s up to us to decide which one to use based on the trade-offs!

🧠 With all that being said, I’ve learned a lot while creating this article and I hope that you’ve managed to learn too or at least to refresh your knowledge on the topic!

❤️ Wish you all productive weekdays and amazing weekend!

--

--

Konstantin Borimechkov

Writing Code For a Living 🚀 | Software Engineer @ Tide | Writing Blogs About Python & CS