Backend Development 11 min read

Configure Atomikos JTA with Spring Boot for Multi‑DataSource Transactions

This guide walks through setting up a Spring Boot project with JDK 8, Atomikos, and JPA to manage two MySQL data sources, configure JTA transaction handling, define entity managers, services, and a controller, and demonstrates successful and failing transaction scenarios with code snippets and screenshots.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Configure Atomikos JTA with Spring Boot for Multi‑DataSource Transactions

Environment

JDK 8+, Atomikos 2.4.12, Spring Boot with JPA.

Dependencies (pom.xml)

<code>&lt;properties&gt;
  &lt;java.version&gt;1.8&lt;/java.version&gt;
&lt;/properties&gt;

&lt;dependencies&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;mysql&lt;/groupId&gt;
    &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;com.alibaba&lt;/groupId&gt;
    &lt;artifactId&gt;fastjson&lt;/artifactId&gt;
    &lt;version&gt;1.2.12&lt;/version&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;com.github.pagehelper&lt;/groupId&gt;
    &lt;artifactId&gt;pagehelper&lt;/artifactId&gt;
    &lt;version&gt;5.2.0&lt;/version&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-jta-atomikos&lt;/artifactId&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
    &lt;scope&gt;test&lt;/scope&gt;
    &lt;exclusions&gt;
      &lt;exclusion&gt;
        &lt;groupId&gt;org.junit.vintage&lt;/groupId&gt;
        &lt;artifactId&gt;junit-vintage-engine&lt;/artifactId&gt;
      &lt;/exclusion&gt;
    &lt;/exclusions&gt;
  &lt;/dependency&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-configuration-processor&lt;/artifactId&gt;
    &lt;optional&gt;true&lt;/optional&gt;
  &lt;/dependency&gt;
&lt;/dependencies&gt;</code>

Application Configuration (application.yml)

<code>server:
  port: 9101
---
spring:
  application:
    name: atomikos
---
spring:
  jta:
    enabled: true
  datasource:
    account:
      resourceName: accountResource
      dsClassName: com.mysql.cj.jdbc.MysqlXADataSource
      url: jdbc:mysql://localhost:3306/account?serverTimezone=GMT+8&amp;useSSL=false
      username: root
      password: 123123
    storage:
      resourceName: storageResource
      dsClassName: com.mysql.cj.jdbc.MysqlXADataSource
      url: jdbc:mysql://localhost:3306/storage?serverTimezone=GMT+8&amp;useSSL=false
      username: root
      password: 123123
---
spring:
  jpa:
    generateDdl: false
    hibernate:
      ddlAuto: update
    openInView: true
    show-sql: true
    properties:
      hibernate:
        physical_naming_strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
        dialect: org.hibernate.dialect.MySQL5Dialect
        transaction:
          jta:
            platform: com.pack.jpa.config.AtomikosJtaPlatform
      javax:
        persistence:
          transactionType: JTA
---
debug: false</code>

If physical_naming_strategy is not configured, column names may not match the expected naming convention.

Data Source Property Classes

<code>@Data
public class BaseDataSourceProperties {
  private String resourceName;
  private String dsClassName;
  private String driver;
  private String url;
  private String username;
  private String password;
}</code>
<code>@Component
@ConfigurationProperties(prefix = "spring.datasource.account")
public class AccountDataSourceProperties extends BaseDataSourceProperties {}
</code>
<code>@Component
@ConfigurationProperties(prefix = "spring.datasource.storage")
public class StorageDataSourceProperties extends BaseDataSourceProperties {}
</code>

DataSource Beans

<code>@Configuration
public class DataSourceConfig {

  @Bean(name = "accountDataSource", initMethod = "init", destroyMethod = "close")
  @Primary
  public DataSource accountDataSource(AccountDataSourceProperties props) {
    AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
    ds.setUniqueResourceName(props.getResourceName());
    ds.setXaDataSourceClassName(props.getDsClassName());
    Properties xaProperties = new Properties();
    xaProperties.setProperty("url", props.getUrl());
    xaProperties.setProperty("user", props.getUsername());
    xaProperties.setProperty("password", props.getPassword());
    ds.setXaProperties(xaProperties);
    ds.setMinPoolSize(10);
    ds.setMaxPoolSize(10);
    ds.setBorrowConnectionTimeout(30);
    ds.setMaxLifetime(60);
    ds.setMaintenanceInterval(60);
    return ds;
  }

  @Bean(name = "storageDataSource", initMethod = "init", destroyMethod = "close")
  public DataSource storageDataSource(StorageDataSourceProperties props) {
    AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
    ds.setUniqueResourceName(props.getResourceName());
    ds.setXaDataSourceClassName(props.getDsClassName());
    Properties xaProperties = new Properties();
    xaProperties.setProperty("url", props.getUrl());
    xaProperties.setProperty("user", props.getUsername());
    xaProperties.setProperty("password", props.getPassword());
    ds.setXaProperties(xaProperties);
    ds.setMinPoolSize(10);
    ds.setMaxPoolSize(10);
    ds.setBorrowConnectionTimeout(30);
    ds.setMaxLifetime(60);
    ds.setMaintenanceInterval(60);
    return ds;
  }
}
</code>

JPA Entity Manager Configuration

<code>public class EntityManagerFactoryConfig {

  @Configuration
  @EnableJpaRepositories(
    basePackages = {"com.pack.account.repository"},
    entityManagerFactoryRef = "accountEntityManagerFactory",
    transactionManagerRef = "transactionManager"
  )
  @DependsOn("transactionManager")
  static class AccountEntityManagerFactory {
    @Resource(name = "accountDataSource")
    private DataSource accountDataSource;
    @Resource
    private JpaProperties props;
    private String accountDomainPkg = "com.pack.account.domain";

    @Bean
    @Primary
    public LocalContainerEntityManagerFactoryBean accountEntityManagerFactory(EntityManagerFactoryBuilder builder) {
      return builder
        .dataSource(accountDataSource)
        .packages(accountDomainPkg)
        .persistenceUnit("account")
        .properties(props.getProperties())
        .build();
    }
  }

  @Configuration
  @EnableJpaRepositories(
    basePackages = {"com.pack.storage.repository"},
    entityManagerFactoryRef = "storageEntityManagerFactory",
    transactionManagerRef = "transactionManager"
  )
  @DependsOn("transactionManager")
  static class StorageEntityManagerFactory {
    @Resource(name = "storageDataSource")
    private DataSource storageDataSource;
    @Resource
    private JpaProperties props;
    private String storageDomainPkg = "com.pack.storage.domain";

    @Bean
    public LocalContainerEntityManagerFactoryBean storageEntityManagerFactory(EntityManagerFactoryBuilder builder) {
      return builder
        .dataSource(storageDataSource)
        .packages(storageDomainPkg)
        .persistenceUnit("storage")
        .properties(props.getProperties())
        .build();
    }
  }
}
</code>

Transaction Management

<code>public class AtomikosJtaPlatform extends AbstractJtaPlatform {
  private static final long serialVersionUID = 1L;
  public static TransactionManager transactionManager;
  public static UserTransaction transaction;

  @Override
  protected TransactionManager locateTransactionManager() {
    return transactionManager;
  }

  @Override
  protected UserTransaction locateUserTransaction() {
    return transaction;
  }
}
</code>
<code>@Configuration
public class TxConfig {

  @Bean(name = "userTransaction")
  public UserTransaction userTransaction() throws Throwable {
    UserTransactionImp userTransactionImp = new UserTransactionImp();
    userTransactionImp.setTransactionTimeout(10000);
    return userTransactionImp;
  }

  @Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close")
  public TransactionManager atomikosTransactionManager() throws Throwable {
    UserTransactionManager userTransactionManager = new UserTransactionManager();
    userTransactionManager.setForceShutdown(false);
    return userTransactionManager;
  }

  @Bean(name = "transactionManager")
  @DependsOn({"userTransaction", "atomikosTransactionManager"})
  public PlatformTransactionManager transactionManager() throws Throwable {
    UserTransaction userTransaction = userTransaction();
    TransactionManager atomikosTransactionManager = atomikosTransactionManager();
    return new JtaTransactionManager(userTransaction, atomikosTransactionManager);
  }
}
</code>

The AtomikosJtaPlatform class is the custom JTA platform referenced in the application.yml configuration.

Entity Classes

<code>@Entity
@Table(name = "t_account")
public class Account {
  @Id
  private Long id;
  private String userId;
  private BigDecimal money;
}
</code>
<code>@Entity
@Table(name = "t_storage")
public class Storage {
  @Id
  private Long id;
  private String commodityCode;
  private Integer count;
}
</code>

Repositories and Services

<code>public interface AccountRepository extends JpaRepository<Account, Long>, JpaSpecificationExecutor<Account> {}
public interface StorageRepository extends JpaRepository<Storage, Long>, JpaSpecificationExecutor<Storage> {}
</code>
<code>@Service
public class AccountService {
  @Resource
  private AccountRepository accountRepository;

  @Transactional
  public void saveAccount(Account account) {
    accountRepository.save(account);
  }
}
</code>
<code>@Service
public class StorageService {
  @Resource
  private StorageRepository storageRepository;

  @Transactional
  public void saveStorage(Storage storage) {
    storageRepository.save(storage);
  }
}
</code>
<code>@Service
public class OperatorService {
  @Resource
  private AccountService accountService;
  @Resource
  private StorageService storageService;

  @Transactional
  public void save(Account account, Storage storage) {
    accountService.saveAccount(account);
    if (storage.getCount() == 0) {
      throw new RuntimeException("库存数量错误0");
    }
    storageService.saveStorage(storage);
    if (storage.getCount() == -1) {
      throw new RuntimeException("库存数量错误-1");
    }
  }
}
</code>

The @Transactional annotation on OperatorService.save ensures that if any exception occurs, the whole transaction is rolled back.

Controller and Test Cases

<code>@RestController
@RequestMapping("/oc")
public class OperatorController {
  @Resource
  private OperatorService os;

  @PostMapping("/save")
  public Object save(@RequestBody OperatorDTO dto) {
    os.save(dto.getAccount(), dto.getStorage());
    return "1";
  }
}
</code>

Successful execution inserts both records; failure (e.g., setting count to 0 or -1) triggers an exception and rolls back the transaction.

When the transaction fails, no data is persisted to the database.

Spring BootTransaction Managementmulti-datasourceJPAAtomikosJTA
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.