Mastering Distributed Transactions in Spring Boot with JTA and Atomikos
This guide explains how to achieve transactional consistency across multiple databases in a Spring Boot application by integrating JTA with Atomikos, covering configuration, code implementation, testing, and verification of distributed transactions using multi‑data‑source setups.
In a Spring Boot project, connecting multiple data sources is common, but ensuring transactional consistency across them (e.g., creating an order while deducting inventory) requires a distributed transaction solution.
When both operations reside in the same database, standard transaction management suffices, but across different databases it fails.
What is JTA
JTA (Java Transaction API) provides stronger transaction capabilities than JDBC, allowing a single transaction to involve multiple participants.
Spring Boot 2.x integrates two JTA implementations: Atomikos and Bitronix. Since Bitronix is deprecated after Spring Boot 2.3.0, this guide uses Atomikos.
Hands‑on
Follow these steps to implement JTA in Spring Boot:
Add the Atomikos starter dependency to pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>Configure two data sources in application.properties using Atomikos properties:
spring.jta.enabled=true
spring.jta.atomikos.datasource.primary.xa-properties.url=jdbc:mysql://localhost:3306/test1
spring.jta.atomikos.datasource.primary.xa-properties.user=root
spring.jta.atomikos.datasource.primary.xa-properties.password=12345678
spring.jta.atomikos.datasource.primary.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource
spring.jta.atomikos.datasource.primary.unique-resource-name=test1
spring.jta.atomikos.datasource.primary.max-pool-size=25
spring.jta.atomikos.datasource.primary.min-pool-size=3
spring.jta.atomikos.datasource.primary.max-lifetime=20000
spring.jta.atomikos.datasource.primary.borrow-connection-timeout=10000
spring.jta.atomikos.datasource.secondary.xa-properties.url=jdbc:mysql://localhost:3306/test2
spring.jta.atomikos.datasource.secondary.xa-properties.user=root
spring.jta.atomikos.datasource.secondary.xa-properties.password=12345678
spring.jta.atomikos.datasource.secondary.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource
spring.jta.atomikos.datasource.secondary.unique-resource-name=test2
spring.jta.atomikos.datasource.secondary.max-pool-size=25
spring.jta.atomikos.datasource.secondary.min-pool-size=3
spring.jta.atomikos.datasource.secondary.max-lifetime=20000
spring.jta.atomikos.datasource.secondary.borrow-connection-timeout=10000Create a configuration class to define the two Atomikos data sources and corresponding JdbcTemplate beans:
@Configuration
public class DataSourceConfiguration {
@Primary
@Bean
@ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.primary")
public DataSource primaryDataSource() {
return new AtomikosDataSourceBean();
}
@Bean
@ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.secondary")
public DataSource secondaryDataSource() {
return new AtomikosDataSourceBean();
}
@Bean
public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource") DataSource primaryDataSource) {
return new JdbcTemplate(primaryDataSource);
}
@Bean
public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
return new JdbcTemplate(secondaryDataSource);
}
}Implement a service that performs updates on both databases within a JTA transaction:
@Service
public class TestService {
private JdbcTemplate primaryJdbcTemplate;
private JdbcTemplate secondaryJdbcTemplate;
public TestService(JdbcTemplate primaryJdbcTemplate, JdbcTemplate secondaryJdbcTemplate) {
this.primaryJdbcTemplate = primaryJdbcTemplate;
this.secondaryJdbcTemplate = secondaryJdbcTemplate;
}
@Transactional
public void tx() {
// update test1
primaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "aaa");
// update test2
secondaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "aaa");
}
@Transactional
public void tx2() {
// update test1
primaryJdbcTemplate.update("update user set age = ? where name = ?", 40, "aaa");
// simulate failure before updating test2
throw new RuntimeException();
}
}Create unit tests to verify successful commits and rollbacks:
@SpringBootTest(classes = Chapter312Application.class)
public class Chapter312ApplicationTests {
@Autowired
protected JdbcTemplate primaryJdbcTemplate;
@Autowired
protected JdbcTemplate secondaryJdbcTemplate;
@Autowired
private TestService testService;
@Test
public void test1() throws Exception {
// successful update
testService.tx();
Assertions.assertEquals(30, primaryJdbcTemplate.queryForObject("select age from user where name=?", Integer.class, "aaa"));
Assertions.assertEquals(30, secondaryJdbcTemplate.queryForObject("select age from user where name=?", Integer.class, "aaa"));
}
@Test
public void test2() throws Exception {
// update fails and should roll back
try {
testService.tx2();
} catch (Exception e) {
// expected
} finally {
Assertions.assertEquals(30, primaryJdbcTemplate.queryForObject("select age from user where name=?", Integer.class, "aaa"));
Assertions.assertEquals(30, secondaryJdbcTemplate.queryForObject("select age from user where name=?", Integer.class, "aaa"));
}
}
}Testing Verification
Run the unit tests and observe Atomikos initialization logs:
Transaction logs are also generated under the transaction-logs directory, showing commit and rollback details.
Code Examples
The full source code can be found in the chapter3-12 directory of the repository:
GitHub: https://github.com/dyc87112/SpringBoot-Learning/
Gitee: https://gitee.com/didispace/SpringBoot-Learning/
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
