Declarative Transaction Management with Spring Framework

Getting declarative transaction management to work with Spring can be a slightly challenging task.

Posted on October 5, 2018

StackOverflow.com, the question and answer website frequented by programmers is full of posts about the @Transactional annotation provided by the Spring Framework "not working". This blog post attempts to summarize the various reasons why the said annotation "does not work" at times, along with relevant "solutions".

1. Have transactions been enabled?

A common assumption among new users of the Spring framework is that the @Transactional annotation "just works out of the box", that is, simply annotating a class or a method with the said annotation is sufficient to gain transactional semantics.

Alas, that is not the case. As explained in the official documentation, declarative transaction management needs to be enabled explicitly for a Spring application, for transaction semantics to be applied automatically using the @Transactional annotation.

1.1. Enabling transactions in Spring Boot

When using Spring Boot with auto-configuration enabled using @SpringBootApplication, transaction management is enabled automatically, and no additional configuration is required.

1.2. Enabling transactions with Java configuration

Transaction management can be enabled with Java configuration by annotating the configuration class (the one annotated with @Configuration) with @EnableTransactionManagement, as shown below:

              
  @Configuration
  @EnableTransactionManagement
  public class ApplicationConfiguration {
    @Bean
    public PlatformTransactionManager transactionManager() {
      // Return an appropriate subclass of PlatformTransactionManager.
    }

    ...
  }
              
            

1.3. Enabling transactions with XML configuration

In order to enable transaction management with XML configuration, the line <transaction:annotation-driven /> must be added to the root context configuration for the application, as shown below:

              
  <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:transaction="http://www.springframework.org/schema/tx"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    <bean id="transactionManager" class="[a subclass of PlatformTransactionManager]" />

    <transaction:annotation-driven />

    ...
  </beans>
              
            

2. Have transactions been enabled correctly?

A point oft-missed about web applications developed using the Spring framework (other than Spring Boot) is that such applications have two Spring contexts - the servlet context, and the root context. The snippet below from a web.xml file makes this distinction clear:

              
  <web-app>
    <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath*:root-context.xml</param-value>
    </context-param>
    <listener>
      <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet>
      <servlet-name>dispatcher-servlet</servlet-name>
      <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
      <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:servlet-context.xml</<param-value>
      </init-param>
      <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
      <servlet-name>dispatcher-servlet</servlet-name>
      <url-pattern>/</url-pattern>
    </servlet-mapping>
  </web-app>
              
            

The bottom part of the file (comprising of the servlet and servlet-mapping sections) creates and initializes the servlet context.

The top part of the file (comprising of the context-param and listener sections) creates and initializes the root context. It also attaches the root context to the servlet context so that Spring beans invoked by the dispatcher servlet can access other Spring beans outside the web layer as well.

If the web layer should control transaction boundaries, transaction management should be enabled through the servlet context. This would be the case for example when controller classes and/or methods need to be/are annotated with @Transactional.

If on the other hand the transaction boundaries lie in layers below the web layer, transaction management should be enabled through the root context.


3. The tale of (two) @Transactional(s)

The Spring @Transactional annotation can be found in the spring-tx.jar file. When using a build tool such as Maven or Gradle, this file also imports the javax.transaction-api.jar file. Unfortunately, this later file also contains a @Transactional annotation (@javax.transaction.Transactional to be precise).

The recent versions of the Spring framework allow using either of these two annotations for declarative transaction management. However, older versions of the framework do not support the later annotation. Developers using auto-import feature of code editors may find their editor importing the later annotation, which Spring framework may not recognize.

Developers must therefore make sure to use @org.springframework.transaction.annotation.Transactional to enforce declarative transaction management.


4. Internal method calls

When declarative transaction management is enabled using (just) @EnableTransactionManagement or <transaction:annotation-driven />, Spring generates proxies for the classes for which transaction management is required (annotated with @Transactional). As explained in the official documentation, the default proxies can only intercept method calls coming from outside the implementation class. This means, transaction semantics will not work for the following case:

              
  @Component
  public class FooComponent {
    public void bar() {
      this.baz();
    }

    @Transactional
    protected void baz() {
      ...
    }
  }
              
            

If such a case needs to be supported, consider using AspectJ based proxies by including spring-aspectjs.jar on the classpath and enabling transaction management as @EnableTransactionManagement(mode = AdviceMode.ASPECTJ) (with Java configuration) or <transaction:annotation-driven mode="aspectj" /> (with XML configuration).


5. To inherit (or not!)

Consider the following Java code:

              
  @Transactional
  public interface FooService {
    void bar();
  }

  @Component
  public class FooServiceImpl implements FooService {
    public void bar() {
      ...
    }
  }
              
            

Developers expect FooServiceImpl.bar() to be transactional. However, they will find this not to be case when declarative transaction management is enabled using @EnableTransactionManagement(mode = AdviceMode.ASPECTJ) or <transaction:annotation-driven mode="aspectj" />. As explained in the official documentation, Java annotations are not inherited by classes from the interfaces they implement. The consequence of this limitation is that the implementing classes are wrapped in proxies that are not transactional in nature (because the classes, and therefore their proxies are not aware of the @Transactional annotation).

The solution is to move the @Transactional annotation from the interface to the implementing class.


6. To inherit (or not!), part 2

Consider the following Java code (similar to the one above):

              
  public interface FooService {
    void bar();
  }

  @Component
  @Transactional
  public class FooServiceImpl implements FooService {
    public void bar() {
      ...
    }
  }

  @Component
  public class FooConsumer {
    @Autowired
    FooService fooService;

    public void consume() {
      fooService.bar();
    }
  }
              
            

It is intuitively reasonable to expect that a call to FooConsumer.consume() will in turn call FooServiceImpl.bar(), which would be invoked in transactional context. However, this too may not work. This is due to the fact that the default proxies are based on the interfaces, not the implementing classes. In the case above, the proxy is generated for FooService, not FooServiceImpl.

The solution is to generate proxies for the implementing classes instead of the interfaces, as @EnableTransactionManagement(proxyTargetClass = true) (with Java configuration) or <transaction:annotation-driven proxy-target-class="true" /> (with XML configuration).


7. @Transactional does not roll back upon exception

A common situation usually involves code similar to the following:

              
  @Component
  public class FooService {
    @Transactional
    public void bar() {
      baz(1);

      ...
    }

    private void baz(int i) throws Exception {
      if (i < 2) {
        throw new Exception("i must not be less than 2.");
      }

      ...
    }
  }
              
            

It is usually expected that the transaction started by FooService.bar() will get rolled back as soon as FooService.baz() is called, because the later method throws an Exception. However, as explained in the official documentation, the Spring infrastructure rolls a transaction back only in the case of runtime, unchecked exceptions. This means, @Transactional is short for @Transactional(rollbackFor = RuntimeException.class).

Therefore, changing throw new Exception(...) to throw new RuntimeException(...) should mark the transaction to roll back automatically.

Alternatively, changing @Transactional to @Transactional(rollbackFor = Exception.class) should also mark the transaction to roll back automatically, although the former approach is preferable to this, as per the Spring framework's philosophy of using only runtime exceptions.


8. @Transactional does not roll back upon runtime exception

Yet another common problem faced by many developers is that of transactions not rolling back, despite following all the steps described above.

In most cases, the problem is one of the following.

a. Underlying data store does not support transactions

Quite a few data stores, most notably the MySQL ISAM engine, and older versions of MongoDB do not support transactions. Therefore, attempting to enlist such data stores in a transactional context will have no effect.

b. Operations involve more than one data store

Sometimes a transaction can span more than one data store, for example, two different database schemas, a database schema and a JMS queue, a JMS queue and an ERP system. In such cases, even though the individual data stores support transactions, users may find that an operation spanning multiple data stores is not atomic. This usually manifests itself as data getting saved to one data store but not the other.

For such cases, developers need to switch to XA transactions.


References

  1. Declarative transaction management with the Spring framework
  2. Proxying mechanisms in the Spring framework