Best Practices for Using @Transactional in Spring Boot

Anil GulatiAnil Gulati
5 min read

Understanding Transaction Management in Spring Boot

Transaction management in Spring Boot is crucial for ensuring data integrity and consistency in your applications. The Transaction Manager plays a pivotal role in managing transactions. Here’s what you need to know:

Role of TransactionManager

When a method annotated with @Transactional is called, Spring Boot utilizes the TransactionManager to create or join a transaction. The Transaction Manager oversees the transaction's lifecycle, including committing or rolling it back based on the outcome of the operation.

Transaction Isolation Levels

Spring Boot supports various transaction isolation levels, including READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, and SERIALIZABLE. These levels determine how transactions interact with each other and the underlying data. Choose the right isolation level for your application’s needs.

@Service
public class UserService {
  @Autowired
  private UserRepository userRepository;
jj@Transactional
  public void updateUser(String username, String email) {
    User user = userRepository.findByUsername(username);
    user.setEmail(email);
    // ... other operations
  }
}

In the example above, updateUser() is marked with @Transactional, allowing Spring Boot to manage the transaction's behavior.

Understanding Transaction Propagation

Transactional behaviour can vary depending on how methods are annotated. Here’s a key distinction:

@Transactional vs. @Transactional(propagation = Propagation.REQUIRES_NEW)

  • @Transactional creates or joins a transaction.

  • @Transactional(propagation = Propagation.REQUIRES_NEW) creates a new transaction, suspending the current one if it exists.

@Service
public class MyService {
@Transactional
    public void methodA() {
        // ... some code here
        methodB();
        // ... some code here
    }
@Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // ... some code here
    }
}

In this example, methodA() invokes methodB(). methodA()'s transaction is suspended when methodB() begins a new transaction due to the REQUIRES_NEW propagation setting.

Handling Transactions Within the Same Class

Spring’s default behavior when a @Transactional method calls another @Transactional method within the same class is noteworthy:

Default Behavior

By default, Spring uses a “proxy-based” approach. If a @Transactional method calls another @Transactional method within the same class, the transactional behaviour isn't applied.

@Service
public class MyService {
    @Autowired
    private MyService self;
ja   @Transactional
    public void methodA() {
        // ... some code here
        self.methodB();
        // ... some code here
    }
@Transactional
    public void methodB() {
        // ... some code here
    }
}

In this example, methodA() and methodB() are both marked with @Transactional. However, due to the "proxy-based" approach, the transactional behaviour isn't applied to methodB() when called from methodA(). To resolve this, consider AspectJ-based weaving or moving the @Transactional method to a separate class.

Managing Transactions Across Different Beans

When calling a method on a different bean, Spring creates a new proxy around the target bean, allowing it to manage transactional behaviour:

@Service
public class MyService {
@Autowired
    private OtherService otherService;
@Transactional
    public void methodA() {
        // ... some code here
        otherService.methodB();
        // ... some code here
    }
}
@Service
public class OtherService {
   @Transactional
    public void methodB() {
        // ... some code here
    }
}

In this example, methodA() calls methodB() on a different bean (OtherService). Spring creates a new proxy around OtherService to apply transactional behaviour based on methodA()Propagation settings.

Handling Unchecked Exceptions

When a @Transactional method throws an unchecked exception, Spring automatically rolls back the transaction by default. This ensures that data changes within the transaction are not persisted if an error occurs.

@Service
@Transactional
public class UserService {
 @Autowired
  private UserRepository userRepository;
 public void updateUser(String username, String email) {
    User user = userRepository.findByUsername(username);
    if (user == null) {
      throw new RuntimeException("User not found");
    }
    user.setEmail(email);
    userRepository.save(user);
    throw new RuntimeException("Something went wrong");
  }
}

In this example, updateUser() is marked with @Transactional and throws an unchecked exception. The transaction will roll back by default, discarding the changes made to the user's email address.

Customizing Rollback Behavior

You can customize the rollback behavior using the rollbackFor or noRollbackFor properties of the @Transactional annotation.

@Service
@Transactional(noRollbackFor = RuntimeException.class)
public class UserService { 
// ...
}

In this example, we specify that a RuntimeException should not trigger a rollback. This can be useful when you want to keep changes within the transaction, even if an error occurs.

Default Rollback Behavior

By default, a @Transactional method rolls back the transaction on any unchecked exception. Customize this behavior using rollbackFor or noRollbackFor properties.

Private Methods and @Transactional

@Transactional works only on public methods. Spring creates a proxy around public methods to manage transactional behaviour. Private methods are not visible to the proxy and cannot be wrapped in a transactional context.

@Service
public class MyService {
   @Transactional
    public void methodA() {
        // ... some code here
        methodB();
        // ... some code here
    }
    private void methodB() {
        // ... some code here
    }
}

In this example, methodA() is marked with @Transactional, but methodB() is not. To enable transactional behaviour for methodB(), make it public or move the @Transactional annotation to a method that calls both methodA() and methodB().

Handling Concurrency Issues

Spring Boot’s @Transactional annotation provides a mechanism for handling concurrency issues by serializing transactions. The default isolation level prevents most concurrency problems by ensuring that transactions do not interfere with each other.

@Service
public class UserService {
  @Autowired
  private UserRepository userRepository;
 @Transactional
  public void updateUser(String username, String email) {
    User user = userRepository.findByUsername(username);
    user.setEmail(email);
    // ... other operations
  }
}

In this example, updateUser() is marked with @Transactional, and Spring ensures that transactions are serialized when multiple threads attempt to modify the same user's email address concurrently. This prevents data inconsistencies and race conditions.

Remember that the default isolation level used by @Transactional in Spring is Isolation.DEFAULT, which aligns with the underlying data source's default. This typically results in "read committed" isolation, which is suitable for most databases.

Mastering @Transactional is crucial for effective transaction management in Spring Boot applications. These best practices will help you handle transactions with confidence and precision.

Enjoyed This Article?

If you enjoy reading this post, got help, and knowledge through it, and want to support my efforts please clap this article and follow. Click Here To Subscribe For NewsletterTo explore more of my work, visit my portfolio at anilgulati.com

44
Subscribe to my newsletter

Read articles from Anil Gulati directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Anil Gulati
Anil Gulati

Associate Software Engineer @YMSLI | Ex-SDE@Onelab Ventures | Full Stack Developer| Freelancer | Bibliophile | Technical Head @DITU ACM SC |