Backend Development 14 min read

Implementing Dynamic Data Source Switching in Spring Boot with ThreadLocal and AbstractRoutingDataSource

This article demonstrates how to implement dynamic data source switching in a Spring Boot application by leveraging ThreadLocal and AbstractRoutingDataSource, providing step‑by‑step code examples for context holders, custom routing, annotation‑driven switching, and runtime addition of new data sources.

Architect's Guide
Architect's Guide
Architect's Guide
Implementing Dynamic Data Source Switching in Spring Boot with ThreadLocal and AbstractRoutingDataSource

When a business requirement involves fetching data from multiple databases and writing it into a current database, dynamic data source switching becomes necessary. The author initially tried the dynamic-datasource-spring-boot-starter but faced environment issues, so they implemented their own solution using ThreadLocal and AbstractRoutingDataSource .

1. Introduction

ThreadLocal provides a thread‑local variable, ensuring each thread accesses its own copy of a variable, which isolates data and reduces synchronization overhead. AbstractRoutingDataSource selects the current data source based on a user‑defined lookup key via the determineCurrentLookupKey() method.

2. Code Implementation

2.1 ThreadLocal Context Holder

/**
 * @author: jiangjs
 * @description:
 * @date: 2023/7/27 11:21
 */
public class DataSourceContextHolder {
    private static final ThreadLocal
DATASOURCE_HOLDER = new ThreadLocal<>();

    public static void setDataSource(String dataSourceName){
        DATASOURCE_HOLDER.set(dataSourceName);
    }

    public static String getDataSource(){
        return DATASOURCE_HOLDER.get();
    }

    public static void removeDataSource(){
        DATASOURCE_HOLDER.remove();
    }
}

2.2 Custom AbstractRoutingDataSource

/**
 * @author: jiangjs
 * @description: Implement dynamic data source routing
 * @date: 2023/7/27 11:18
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources){
        super.setDefaultTargetDataSource(defaultDataSource);
        super.setTargetDataSources(targetDataSources);
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

2.3 Configuration (application.yml and Java Config)

# application.yml snippet
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      master:
        url: jdbc:mysql://xxxxxx:3306/test1?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
      slave:
        url: jdbc:mysql://xxxxx:3306/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
      ...

@Configuration
public class DateSourceConfig {
    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    public DataSource slaveDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource createDynamicDataSource(){
        Map<Object,Object> dataSourceMap = new HashMap<>();
        DataSource defaultDataSource = masterDataSource();
        dataSourceMap.put("master", defaultDataSource);
        dataSourceMap.put("slave", slaveDataSource());
        return new DynamicDataSource(defaultDataSource, dataSourceMap);
    }
}

2.4 Testing Basic Switching

@GetMapping("/getData.do/{datasourceName}")
public String getMasterData(@PathVariable("datasourceName") String datasourceName){
    DataSourceContextHolder.setDataSource(datasourceName);
    TestUser testUser = testUserMapper.selectOne(null);
    DataSourceContextHolder.removeDataSource();
    return testUser.getUserName();
}

Running the endpoint with master or slave returns data from the corresponding database, confirming the dynamic routing works.

2.5 Annotation‑Driven Switching

Define a custom annotation @DS and an AOP aspect to set and clear the data source automatically.

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DS {
    String value() default "master";
}

@Aspect
@Component
@Slf4j
public class DSAspect {
    @Pointcut("@annotation(com.jiashn.dynamic_datasource.dynamic.aop.DS)")
    public void dynamicDataSource(){}

    @Around("dynamicDataSource()")
    public Object datasourceAround(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        DS ds = method.getAnnotation(DS.class);
        if (Objects.nonNull(ds)) {
            DataSourceContextHolder.setDataSource(ds.value());
        }
        try {
            return point.proceed();
        } finally {
            DataSourceContextHolder.removeDataSource();
        }
    }
}

Controller methods can now be annotated with @DS("slave") to switch to the slave data source without manual code.

2.6 Runtime Addition of New Data Sources

A DataSourceEntity class stores connection details, and DynamicDataSource gains a createDataSource(List ) method that validates, builds, and registers new data sources into the internal map.

public void createDataSource(List
dataSources){
    if (CollectionUtils.isNotEmpty(dataSources)) {
        for (DataSourceEntity ds : dataSources) {
            Class.forName(ds.getDriverClassName());
            DriverManager.getConnection(ds.getUrl(), ds.getUserName(), ds.getPassWord());
            DruidDataSource dataSource = new DruidDataSource();
            BeanUtils.copyProperties(ds, dataSource);
            dataSource.setTestOnBorrow(true);
            dataSource.setTestWhileIdle(true);
            dataSource.setValidationQuery("select 1 ");
            dataSource.init();
            this.targetDataSourceMap.put(ds.getKey(), dataSource);
        }
        super.setTargetDataSources(this.targetDataSourceMap);
        super.afterPropertiesSet();
    }
}

A CommandLineRunner implementation reads a test_db_info table at startup, converts each row to a DataSourceEntity , and calls dynamicDataSource.createDataSource(ds) , making the new sources immediately usable.

Subsequent API calls with the newly added source name retrieve data correctly, proving that data sources can be added dynamically at runtime.

Overall, the tutorial provides a complete guide—from basic ThreadLocal usage to annotation‑driven switching and dynamic addition—enabling developers to manage multiple databases efficiently within a Spring Boot application.

AOPdatabaseSpringBootMyBatis-PlusthreadlocalAbstractRoutingDataSourceDynamicDataSource
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

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.