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)

<properties>
  <java.version>1.8</java.version>
</properties>

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

Application Configuration (application.yml)

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&useSSL=false
      username: root
      password: 123123
    storage:
      resourceName: storageResource
      dsClassName: com.mysql.cj.jdbc.MysqlXADataSource
      url: jdbc:mysql://localhost:3306/storage?serverTimezone=GMT+8&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

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

Data Source Property Classes

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

DataSource Beans

@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;
  }
}

JPA Entity Manager Configuration

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();
    }
  }
}

Transaction Management

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;
  }
}
@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);
  }
}

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

Entity Classes

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

Repositories and Services

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

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

  @Transactional
  public void saveStorage(Storage storage) {
    storageRepository.save(storage);
  }
}
@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");
    }
  }
}

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

Controller and Test Cases

@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";
  }
}

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring Boottransaction-managementjpaAtomikosJTAMulti-DataSource
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

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.