Post Thumbnail

Differences between @Transactional and TransactionTemplate in SpringBoot

1. Overview

In this article, we are going to be looking at the difference between the @Transactional annotation and TransactionTemplate in SpringBoot and which one to use in certain circumstances.

A database transaction enables us to safely execute multiple SQL statements with the capability to roll back ALL changes in case any of failure.

For instance, in a banking application, we will wrap the process to change the balance in the accounts table and update the transactions table in a database transaction. Such that, if the transaction update fails, the balance change will be reverted.

2. Transaction Scope

One of the main differences between @Transactional and TransactionTemplate is their scope of coverage.

The @Transactional annotation is always added to a method, and thus it covers ALL database UPDATE/INSERT operations within that method.

Listing 2.1 UserService.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Transactional
public User createTransactional(String firstName, String lastName, String email) {

    User user = new User();
    user.setFirstName(firstName);
    user.setLastName(lastName);
    user.setEmail(email);
    user = userRepository.save(user);

    //save default user notification preference
    UserNotificationPreference preference = new UserNotificationPreference();
    preference.setUser(user);
    preference.setEmailNotificationEnabled(true);
    userNotificationPreferenceRepo.save(preference);

    //save audit trail
    AuditTrail auditTrail = new AuditTrail();
    auditTrail.setAction("USER CREATED");
    auditTrail.setActor("Logged In User");
    auditTrailRepository.save(auditTrail);
    
    //send welcome email
    emailService.sendWelcomeEmail(user);

    return user;
}

If an exception were to be thrown while saving the AuditTrail record, the User and UserNotificationPreference records would be rolled back.

TransactionTemplate on the other hand, is a utility class that accepts an implementation of the TransactionCallback interface for execution.

This means ALL the UPDATE/INSERT operations taking place in an implementation of a TransactionCallback will be wrapped in a database operation.

It goes without saying, that database operations outside the callback implementation will not be covered in a transaction.

Listing 2.2 UserService.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public User createTransactionTemplate(String firstName, String lastName, String email) {

    User user = new User();
    user.setFirstName(firstName);
    user.setLastName(lastName);
    user.setEmail(email);


    //save default user notification preference
    UserNotificationPreference preference = new UserNotificationPreference();
    preference.setUser(user);
    preference.setEmailNotificationEnabled(true);

    //wrap the call to save in a transactionTemplate
    transactionTemplate.execute(status -> {
        userRepository.save(user);
        userNotificationPreferenceRepo.save(preference);
        status.flush();
        return null;

    });

    //this operation is not included in the DB transaction
    AuditTrail auditTrail = new AuditTrail();
    auditTrail.setAction("USER CREATED");
    auditTrail.setActor("Logged In User");
    auditTrailRepository.save(auditTrail);

    //send welcome email
    emailService.sendWelcomeEmail(user);

    return user;
}

If an exception were to arise while saving the AuditTrail record, the User and UserNotificationPreference records will still exist in the database and will not be rolled back.

This is because the saving of the AuditTrail is outside the scope of the transactionTemplate.execute statement.

3. Point of Execution

SQL statements generated in a method annotated with @Transactional are run in the underlying Database at the end of the method or when it returns. Whereas TransactionTemplate executes its own SQL statements immediately.

This difference between these points of executions makes a world of difference. We will use the methods created in Listing 2.1 and 2.2 above to illustrate this difference.

Listing 3.1 UserServiceUnitTest.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Test
void givenDuplicateEmail_whenCreateTransactional_thenSendEmailTwiceAndThrowException() {

        var faker = Faker.instance();

        String firstName = faker.name().firstName();
        String lastName = faker.name().lastName();
        String email = faker.internet().emailAddress();

        //create first user record
        userService.createTransactional(firstName, lastName, email);

        //attempting to create another user with the same email
        Assertions.assertThrows(DataIntegrityViolationException.class,
        () -> userService.createTransactional(firstName, lastName, email));

        //email is sent twice, despite the second attempt failed
        Mockito.verify(emailService, Mockito.times(2))
        .sendWelcomeEmail(Mockito.any(User.class));
}

From the snippet above, we attempted to create two User records with the same email. This of course will trigger an exception because the email field is unique.

Despite the exception, emailService.sendWelcomeEmail(user); was still invoked twice - one for each attempt.

This is because the actual saving of the record in the database happens at the end of the method, by which time the email has been sent out already.

As expected, the second attempt will lead to a database exception and rollback the saved record, thereby creating a situation where the User will get a welcome email but their record will not exist in our database.

Let’s observe the same logic but with the help of a TransactionTemplate.

Listing 3.2 UserServiceUnitTest.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Test
void givenDuplicateEmail_whenCreateTransactionTemplate_thenSendEmailOnceAndThrowException() {

    var faker = Faker.instance();

    String firstName = faker.name().firstName();
    String lastName = faker.name().lastName();
    String email = faker.internet().emailAddress();

    //create first user record
    userService.createTransactionTemplate(firstName, lastName, email);

    //attempting to create another user with the same email
    Assertions.assertThrows(DataIntegrityViolationException.class,
            () -> userService.createTransactionTemplate(firstName, lastName, email));

    //email is only sent for the first attempt
    Mockito.verify(emailService, Mockito.times(1))
            .sendWelcomeEmail(Mockito.any(User.class));
}

From the test scenario above, we can see that the second attempt to create a User record with the same email failed as expected but the emailService is called only once.

This means, the second user will not get a welcome email and their record will not exist in our database.

Similar scenarios to the above abound in day-to-day Engineering, this is why we need to understand the difference and consciously make the right choice.

4. Tips and Tricks

One of the gotchas to look out for while using @Transactional is throwing an exception in the annotated method.

From Listing 2.1 above, if emailService.sendWelcomeEmail(user); throws an uncaught runtime exception, the created User record will be rolled back.

This is despite that the sending of a welcome email is the last action in that method.

Listing 4.1 UserServiceUnitTest.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Test
void givenEmailServiceThrowsException_whenCreateTransactional_thenRollbackCreatedUser() {

    var faker = Faker.instance();

    String firstName = faker.name().firstName();
    String lastName = faker.name().lastName();
    String email = faker.internet().emailAddress();

    //mock the emailService to throw runtime exception
    Mockito.doThrow(new RuntimeException("EmailService failed"))
            .when(emailService).sendWelcomeEmail(Mockito.any(User.class));

    assertThrows(RuntimeException.class,
            () -> userService.createTransactional(firstName, lastName, email));

    boolean recordCreated = userRepository.existsByEmail(email);
    assertFalse(recordCreated);

}

However, if we are to use the TransactionTemplate version, the already created record will still exist in the database.

Listing 4.2 UserServiceUnitTest.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

@Test
void givenEmailServiceThrowsException_whenCreateTransactionTemplate_thenSaveUser() {

    var faker = Faker.instance();

    String firstName = faker.name().firstName();
    String lastName = faker.name().lastName();
    String email = faker.internet().emailAddress();

    //mock the emailService to throw runtime exception
    Mockito.doThrow(new RuntimeException("EmailService failed"))
            .when(emailService).sendWelcomeEmail(Mockito.any(User.class));

    assertThrows(RuntimeException.class,
            () -> userService.createTransactionTemplate(firstName, lastName, email));

    boolean recordCreated = userRepository.existsByEmail(email);
    assertTrue(recordCreated);

}

These types of differences do lead to sometimes obscure bugs in the codebase, especially in complex business logic that involves multiple levels of method calls.

Note that there’s nothing wrong with the first behaviour if that is what we want. Also, if emailService.sendWelcomeEmail(user); is an async method, an exception in it will not lead to a rollback of the transaction as we observed in Listing 4.1

5. Conclusion

Through different examples, we have looked at the difference between @Transactional and TransactionTemplate when it comes to supporting database transactions in a Spring Boot application and common pitfalls to avoid.

The complete source code for the examples in this article is on GitHub.

Happy Coding