How to Co‑exist Multiple DataSources in Spring Without Switching?

This guide explains the concept of multi‑DataSource coexistence, when it is appropriate, step‑by‑step configuration for MySQL and TDengine, transaction manager handling, custom annotations, mapper scanning, differences from dynamic routing, common pitfalls, and best‑practice usage in a Spring‑MyBatis backend.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
How to Co‑exist Multiple DataSources in Spring Without Switching?

What Is “Multi‑DataSource Coexistence”?

The core idea is to keep several DataSource beans in the same application, each bound to its own Mapper / SqlSessionFactory / TransactionManager, without using AbstractRoutingDataSource or ThreadLocal switching.

@Resource(name = "mysqlDataSource")
private DataSource mysqlDataSource;

@Resource(name = "tdengineDataSource")
private DataSource tdengineDataSource;

Multiple DataSource beans exist simultaneously

Each bean has its own configuration

Each bean maps to a dedicated Mapper/DAO/Service

No AbstractRoutingDataSource involved

No ThreadLocal switching

Isolation between data sources

Each works independently

Usage syntax is identical to a single‑source setup

When to Use It

Different database types (e.g., MySQL + TDengine, MySQL + Oracle, MySQL + MongoDB)

Distinct responsibilities (business DB, log DB, time‑series DB, analytics DB)

No need for read/write splitting, master‑slave switching, or tenant isolation

Example: a UAV‑tracking system stores core business data in MySQL and massive time‑series logs in TDengine; both databases are required simultaneously.

Configuration Steps

1. Define Two DataSources (using Druid)

spring:
  datasource:
    # MySQL data source (business data)
    mysql:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.112.58:3306/uav-safety?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&rewriteBatchedStatements=true
      username: root
      password: root
      type: com.alibaba.druid.pool.DruidDataSource
      initialSize: 10
      min-idle: 5
      max-active: 20
      max-wait: 60000
      validation-query: SELECT 1
      test-while-idle: true
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      filters: stat,wall,slf4j

    # TDengine data source (time‑series / log)
    tdengine:
      driver-class-name: com.taosdata.jdbc.rs.RestfulDriver
      url: jdbc:TAOS-RS://192.168.112.58:6041/uav_safety?useSSL=false
      username: root
      password: taosdata
      type: com.alibaba.druid.pool.DruidDataSource
      initialSize: 7
      min-idle: 5
      max-active: 20
      max-wait: 60000
      validation-query: SELECT 1
      test-while-idle: true
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 50
      filters: stat

2. MySQL DataSource Configuration Class

@Configuration
@MapperScan(basePackages = "com.za.uav.mapper.mysql", sqlSessionFactoryRef = "mysqlSqlSessionFactory")
public class MysqlDataSourceConfig {

    @Bean(name = "mysqlDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.mysql")
    public DataSource mysqlDataSource() {
        return new com.alibaba.druid.pool.DruidDataSource();
    }

    @Bean(name = "mysqlSqlSessionFactory")
    public SqlSessionFactory mysqlSqlSessionFactory(@Qualifier("mysqlDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        return bean.getObject();
    }

    @Bean(name = "mysqlTransactionManager")
    public DataSourceTransactionManager mysqlTransactionManager(@Qualifier("mysqlDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

Key points : @MapperScan must point to the MySQL mapper package; @ConfigurationProperties automatically binds the Druid settings.

3. TDengine DataSource Configuration Class

@Configuration
@MapperScan(basePackages = "com.za.uav.mapper.tdengine", sqlSessionFactoryRef = "tdengineSqlSessionFactory")
public class TdengineDataSourceConfig {

    @Bean(name = "tdengineDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.tdengine")
    public DataSource tdengineDataSource() {
        return new com.alibaba.druid.pool.DruidDataSource();
    }

    @Bean(name = "tdengineSqlSessionFactory")
    public SqlSessionFactory tdengineSqlSessionFactory(@Qualifier("tdengineDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        return bean.getObject();
    }
}

Key points : Separate package names and bean names keep the two data sources isolated.

4. Transaction Managers

When multiple transaction managers exist, you must specify which one to use:

@Transactional(transactionManager = "mysqlTransactionManager", rollbackFor = Exception.class)
public void saveMysql() {
    mysqlMapper.insert(data);
}

@Transactional(transactionManager = "tdengineTransactionManager", rollbackFor = Exception.class)
public void saveTd() {
    tdMapper.insert(data);
}

If a method touches both databases, a single @Transactional can only bind one manager, so the other database will not roll back on failure.

5. Custom Annotations for Simpler Transaction Declaration

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "mysqlTransactionManager", rollbackFor = Exception.class)
@Documented
public @interface MysqlTx {}

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "tdengineTransactionManager", rollbackFor = Exception.class)
@Documented
public @interface TdTx {}

Usage example:

@MysqlTx
public void saveUser(User user) {
    userMapper.insert(user);
}

@TdTx
public void saveLog(Log log) {
    tdLogMapper.insert(log);
}

6. Handling a Method That Needs Both Databases

public void saveAll() {
    mysqlMapper.insert(...);
    tdMapper.insert(...);
}

Because @Transactional can bind only one manager, you typically use a compensation pattern: commit the MySQL part, then try the TDengine part inside a try/catch and roll back manually if needed.

@Transactional(transactionManager = "mysqlTransactionManager")
public void process() {
    mysqlMapper.insert(order);
    try {
        tdMapper.insert(log);
    } catch (Exception e) {
        // manual compensation, e.g., delete the MySQL record
        mysqlMapper.deleteById(order.getId());
        throw e;
    }
}

7. MapperScan vs. @Mapper

Do not use @Mapper on individual interfaces; instead register the whole package with @MapperScan so that each data source knows which mappers belong to it.

MapperScan configuration diagram
MapperScan configuration diagram

Similarly, keep mapper XML files in separate directories (e.g., mapper/mysql vs. mapper/tdengine).

Mapper XML isolation
Mapper XML isolation

8. Comparison with Dynamic DataSource (Routing)

DataSource count : multiple vs. single external

Switching : none vs. ThreadLocal‑based routing

Typical scenarios : different business databases vs. read/write splitting, multi‑tenant

Recommendation : coexistence is strongly recommended when responsibilities are fixed; dynamic routing only when switching is truly required

9. Practical Usage Example

@Service
public class UserService {

    @Resource
    private UserMapper userMapper;

    @Resource
    private TdLogMapper tdLogMapper;

    @MysqlTx
    public void saveUser(User user) {
        userMapper.insert(user);
    }

    @TdTx
    public void saveLog(Log log) {
        tdLogMapper.insert(log);
    }
}

The service code does not need to know which database is used; the underlying configuration handles the separation.

Common Issues

Druid monitoring not showing TDengine queries : ensure the filters setting does not include wall, which is MySQL‑specific and can cause NPEs.

"Invalid bound statement (not found)" : verify that the mapper interface and its XML file are scanned by the correct @MapperScan and that the method name matches the XML id.

MyBatis‑Plus SqlSessionFactory : when using MyBatis‑Plus, replace SqlSessionFactoryBean with MybatisSqlSessionFactoryBean and set the mapper locations accordingly.

Conclusion

Multi‑DataSource coexistence solves the "how to let each component work with its own dedicated database" problem, while dynamic routing solves the "how to switch" problem. Choosing the right approach depends on whether you need runtime switching or simply clear separation of responsibilities. Proper bean naming, package isolation, and explicit transaction manager selection prevent hidden bugs and keep the system stable.

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.

JavatransactionspringMyBatisMulti-DataSource
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

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.