How Spring @Transactional works?

When using @Transactional on method of a bean or on the bean class, spring will create a Proxy for the bean.

Given following service:

@Service
public class UserServiceImpl implements UserService {

    private UserRepository userRepository;

    public UserServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Transactional
    @Override
    public void insert(UserEntity userEntity)  {
        userRepository.save(userEntity);
    }
}

The generated proxy looks "like" this:

public class UserServiceImpl$$SpringCGLIB$$0 extends UserServiceImpl implements SpringProxy, Advised, Factory {

    // ...
   
    public void insert(UserEntity userEntity)  {
		MethodInterceptor interceptor = this.CGLIB$CALLBACK_0;
		if (interceptor == null) {
			CGLIB$BIND_CALLBACKS(this);
			interceptor = this.CGLIB$CALLBACK_0;
		}

        if (interceptor != null) {
            interceptor.intercept(this, CGLIB$insert$0$Method, new Object[]{var1}, CGLIB$insert$0$Proxy);
        } else {
            super.insert(var1);
        }
    }
    
    // ...
}

In context of transaction management, in code above, interceptor is a TransactionInterceptor
Which performs:

public Object invoke(MethodInvocation invocation) throws Throwable {
	Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

	return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {
//.....
    // nmeylan: platformTransactionManager is JpaTransactionManager when using spring-orm with default config
    // nmeylan: platformTransactionManager is DataSourceTransactionManager when using a jdbc datasource
    TransactionInfo txInfo = createTransactionIfNecessary(platformTransactionManager, txAttr, joinpointIdentification);

    Object retVal;
    try {
        // This is an around advice: Invoke the next interceptor in the chain.
        // This will normally result in a target object being invoked.
        retVal = invocation.proceedWithInvocation();
    }
    catch (Throwable ex) {
        // target invocation exception
        completeTransactionAfterThrowing(txInfo, ex);
        throw ex;
    }
    finally {
        cleanupTransactionInfo(txInfo);
    }
    //.....

Source

Without the proxy we would manually have written this, that is what the proxy does:

public void insert(UserEntity userEntity)  {
    Connection connection = dataSource.getConnection();
    try (connection) {
        connection.setAutoCommit(false);
    
        userRepository.save(userEntity);

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

If we execute the code with a breakpoint in datasource.getConnection() implementation, here the stacktrace we get:

It opens or gets a connection from the connection pool at this moment.

Note that transaction is not opened yet on database server, it comes later with the first query being executed.

Mistakes

With the knowledge of what using @Transactional implies, let see some common mistakes.

Almost all resources on internet highlight that @Transactional should be used at Service and it should not be done at Repository layer because it is the responsibility of the service to orchestrate database call and knows when to open transaction.
Those articles are not wrong because they only takes examples of simple CRUD application where services are pass-through to the database.
The issue with this recommendation is it is not a one-size-fit-all solution, and it should be applied carefully.

The code of below examples for this example can be found on github.

1 - Increasing transaction time

Very often the code wrapped in the transaction is not doing only interaction with database

  • Sometimes it performs expensive operation before or after the database interaction is done
  • Sometimes it is worse and some network calls (e.g: sending email) are performed within the transaction which time taken can be non-predictive.
public class UserServiceImpl implements UserService {

    private UserRepository userRepository;
    private EmailSender emailSender;

    public UserServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Transactional
    @Override
    public void registerUser(UserEntity userEntity)  {
        userRepository.save(userEntity);
        // Just for the sake of example, in real life we prefer to send email to the user once the transaction is commited
        // to not send email if the transaction rollback or fail to commit due to network issue
        emailSender.send(userEntity.getEmail(), "Welcome " + userEntity.getName())");
    }
}

In this case it would have been better to keep transaction management at repository layer by not putting @Transactional on this method, this is handle by spring-data-jpa.

What happens if SMTP server is slow or does not responds with a timeout at 1 minute? The transaction stay open for the whole duration of the registerUser execution.

What are the consequences of a transactions being open when using postgresql:

  • Having a transaction open on a table create Locks, depending on the query being run within the transaction a lock could prevent other query to execute, or prevent ddl changes, checkout https://pglocks.org/ to determine the impact of your query.
  • It consumes memory and cpu on database server.
  • Depending on the query being executed, it prevents maintenance tasks to be run like VACUUM or auto vacuum.

To summarize it bring stability and performance degradation.

2 - Wrongly Use @Transactional at class level

As stated before, many developers are using @Transactional at class level on their services, but it often leads to mistakes.
Sometimes it is even worse, this is done on the interface without knowing what the actual implementation will do (which is the purpose of an interface no?).

Both approach can be justified if you know what you do, one may use @Transactional(propagation = Propagation.NEVER) when transaction are not needed, but you don't know how code will be added** to this class later.

A typical example:

@Service
@Transactional // <- Notice @Transactional at class level
public class UserServiceImpl implements UserService {

    private UserRepository userRepository;

    public UserServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public void insert(UserEntity userEntity)  {
        userRepository.save(userEntity);
    }

    @Override
    public void nameValidation(String name) {
        // Simulating calling a slow external service to check if name is valid
        try {
            sleep(1000 * 60);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

Now when we call nameValidation method, it gets a connection, even if we don't interact with the database.

What are the consequences of getting a connection:

  • When you are using a connection pool you have less active connection available, meaning that you risk to busy all connections of your pool, by calling method not even interacting with the database!
  • When you are not using a connection pool, it means that you highly risk to create too many connections on the database, by calling method not even interacting with the database!

This test shows illustrate it:

public class UserServiceIT extends IntegrationTestClass{
    @Autowired
    UserService userService;
    @Autowired
    HikariDataSource dataSource;

    @Test
    public void nameValidation() throws InterruptedException {
        // Given
        ExecutorService executorService = Executors.newFixedThreadPool(12);
        // When
        for(int i = 0; i < 12; i++) {
           CompletableFuture.runAsync(() -> userService.nameValidation("John doe"), executorService);
        }
        // Waiting few ms to get the MXBean stats refreshed
        Thread.sleep(100);
        // Then
        // max pool size is 10
        assertThat(dataSource.getHikariPoolMXBean().getActiveConnections()).isEqualTo(10);
        assertThat(dataSource.getHikariPoolMXBean().getThreadsAwaitingConnection()).isEqualTo(2);
    }
}

When all connections of the pool are active, code requiring a connection will just wait until it gets a connection or timeout.

Not good for the whole application or product stability and performance, just because @Transactional was wrongly used.

3 - Use @Transactional on private method or calling an internal method

This one is an often visible and well documented mistake.

@Service
public class UserServiceImpl implements UserService {

    private UserRepository userRepository;
    private UserStatsRepository userStatsRepository;

    public UserServiceImpl(UserRepository userRepository, UserStatsRepository userStatsRepository) {
        this.userRepository = userRepository;
        this.userStatsRepository = userStatsRepository;
    }
    
    @Transactional
    @Override
    public void insert(UserEntity userEntity)  {
        userRepository.save(userEntity);
        this.forceIncrementUserInsertStats()
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    private/public void forceIncrementUserInsertStats() {
        userStatsRepository.incNewUserCount();
    }
}

In both case no new transaction will be created when calling forceIncrementUserInsertStats:

  • When method is public calling internal method call the actual method not the proxy one.
  • When method is private there is no proxy generated at all, but also same as for public calling internal method call the actual method not the proxy one.

Correct usage example

Let's finish on a correct usage of @Transactional.

If we take our example above where we wanted to persist a user and send him an email after he is registered in our system.

Requirements are strict:

  • We should guarantee that an email is sent after the user is inserted in our database.
  • We should guarantee to not send email if the user is not actually persisted in database.

For this lets say we are using a library (like jobrunr) which allow to register job in database, and job will be guaranteed to succeed.

We want to perform both insert within the same transaction.

@Service
public class RegisterUserImpl {

    UserPersistence userPersistence;
    
    // This implementation of EmailSender does not actually send email, but register a durable job in database which will send email
    DurableEmailSenderImpl durableEmailSender;

    public RegisterUserImpl(UserPersistence userPersistence, DurableEmailSenderImpl durableEmailSender) {
        this.userPersistence = userPersistence;
        this.durableEmailSender = durableEmailSender;
    }

    @Transactional
    public void register(UserEntity userEntity) {
        userPersistence.save(userEntity);
        durableEmailSender.sendEmail(userEntity.getEmail(), "Welcome " + userEntity.getFirstName());
    }
}

Wrap up

  • @Transactional is very convenient if you know what it implies!
  • Clone code of this article, play with debugger.
  • Take care of your code and database 🙏 use @Transactional carefully, on purpose or "manually manage" transaction via TransactionTemplate to avoid easy mistakes.

I encourage you to read this complete guide about spring transaction management by Marco Behler.

« All posts
Nicolas Meylan © 2024