Backend Development 12 min read

Master Dynamic Multi-DataSource Switching in Spring Boot 2.7

This tutorial explains why enterprise applications need multiple data sources, outlines the scenarios such as business isolation, migration, performance and maintainability, and provides a step‑by‑step implementation of dynamic data source routing in Spring Boot using custom context holders, routing classes, annotations, aspects, configuration, and web‑request control.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Master Dynamic Multi-DataSource Switching in Spring Boot 2.7

Environment

SpringBoot 2.7.16

1. Introduction

Complex enterprise applications often require multiple data sources, each serving a different business module. Common scenarios include:

Business isolation : Separate databases ensure data integrity and security across modules.

Data migration and backup : Easily move or back up data between sources.

Performance and scalability : Deploy modules on different databases for load balancing and horizontal scaling; switch when a database becomes a bottleneck.

Maintainability : Switch to another source when a database fails, and simplify upgrades.

Spring provides dynamic data source switching via AbstractRoutingDataSource , which selects the appropriate source at runtime.

2. Practical Example

2.1 Custom Multi‑DataSource

Thread‑local holder for the current data source:

<code>public class DataSourceContextHolder {
    private static final ThreadLocal<String> HOLDER = new InheritableThreadLocal<>();
    public static void set(String key) { HOLDER.set(key); }
    public static String get() { return HOLDER.get(); }
    public static void clear() { HOLDER.remove(); }
}</code>

Routing datasource extending AbstractRoutingDataSource :

<code>public class PackRoutingDataSource extends AbstractRoutingDataSource {
    private final String defaultDs;
    public PackRoutingDataSource(String defaultDs) { this.defaultDs = defaultDs; }
    @Override
    protected Object determineCurrentLookupKey() {
        String key = DataSourceContextHolder.get();
        if (key == null || "".equals(key)) { key = this.defaultDs; }
        return key;
    }
}</code>

Properties class inheriting from Spring Boot's DataSourceProperties :

<code>public class PackDataSourceProperties extends DataSourceProperties { }</code>

Configuration holder for all data sources:

<code>@Component
@ConfigurationProperties(prefix = "pack.datasource")
public class MultiDataSourceProperties {
    private Map<String, PackDataSourceProperties> config = new HashMap<>();
    private String defaultDs;
}</code>

Spring configuration that creates the routing datasource and registers target datasources:

<code>@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource dataSource(MultiDataSourceProperties properties) {
        PackRoutingDataSource dataSource = new PackRoutingDataSource(properties.getDefaultDs());
        Map<Object, Object> targetDataSources = new HashMap<>();
        properties.getConfig().forEach((key, props) -> {
            targetDataSources.put(key, createDataSource(props, HikariDataSource.class));
        });
        dataSource.setTargetDataSources(targetDataSources);
        return dataSource;
    }
    private static <T> T createDataSource(PackDataSourceProperties props, Class<? extends DataSource> type) {
        return (T) props.initializeDataSourceBuilder().type(type).build();
    }
}</code>

Custom Annotation and Aspect

Annotation to specify a datasource:

<code>@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Ds { String value() default ""; }</code>

Aspect that switches the datasource before method execution and clears the context afterwards:

<code>@Component
@Aspect
public class DataSourceAspect {
    private static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);
    @Resource
    private MultiDataSourceProperties properties;
    @Pointcut("@annotation(ds)")
    private void dsPointcut(Ds ds) {}
    @Around("dsPointcut(ds)")
    public Object around(ProceedingJoinPoint pjp, Ds ds) throws Throwable {
        String key = ds.value();
        if (key == null || "".equals(key)) { key = this.properties.getDefaultDs(); }
        logger.info("current datasource【{}】", key);
        DataSourceContextHolder.set(key);
        try { return pjp.proceed(); }
        finally { DataSourceContextHolder.clear(); }
    }
}</code>

YAML Configuration

<code>pack:
  datasource:
    defaultDs: ds1
    config:
      ds1:
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/ds1?serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&useSSL=false
        username: root
        password: xxoo
        type: com.zaxxer.hikari.HikariDataSource
      ds2:
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/ds2?serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&useSSL=false
        username: root
        password: xxoo
        type: com.zaxxer.hikari.HikariDataSource</code>

Testing with JPA

Entity definition:

<code>@Entity
@Table(name = "t_user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}</code>

Repository and service using the @Ds annotation:

<code>public interface UserRepository extends JpaRepository<User, Long> {}
@Service
public class UserService {
    private final UserRepository userRepository;
    public UserService(UserRepository userRepository) { this.userRepository = userRepository; }
    public User queryUserDefault(Long id) { return userRepository.findById(id).orElse(null); }
    @Ds("ds1")
    public User queryUserDs1(Long id) { return userRepository.findById(id).orElse(null); }
    @Ds("ds2")
    public User queryUserDs2(Long id) { return userRepository.findById(id).orElse(null); }
    @Transactional
    @Ds
    public void saveUser(User user) { userRepository.save(user); }
}</code>

Test cases:

<code>@Resource
private UserService us;
@Test
public void testSave() {
    User user = new User();
    user.setName("张三");
    us.saveUser(user);
}
@Test
public void testQuery() {
    System.out.println(us.queryUserDs1(1L));
    System.out.println(us.queryUserDs2(1L));
    System.out.println(us.queryUserDefault(1L));
}</code>

2.3 Controlling via Web Request

Interceptor that reads a custom header x-ds to set the datasource for the current request:

<code>public class RequestDataSourceInterceptor implements HandlerInterceptor {
    public static final ThreadLocal<String> HOLDER = new InheritableThreadLocal<>();
    private static final String HEADER_KEY = "x-ds";
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        HOLDER.set(request.getHeader(HEADER_KEY));
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        HOLDER.remove();
    }
}</code>

Aspect is updated to prioritize the header value before falling back to the annotation or default datasource:

<code>@Around("dsPointcut(ds)")
public Object around(ProceedingJoinPoint pjp, Ds ds) throws Throwable {
    String key = RequestDataSourceInterceptor.HOLDER.get();
    if (key == null || "".equals(key)) { key = ds.value();
        if (key == null || "".equals(key)) { key = this.properties.getDefaultDs(); }
    }
    // set context and proceed as before
    DataSourceContextHolder.set(key);
    try { return pjp.proceed(); }
    finally { DataSourceContextHolder.clear(); }
}</code>

Example controller that uses the service:

<code>@RestController
public class UserController {
    @Resource
    private UserService us;
    @GetMapping("/{id}")
    public User query(@PathVariable("id") Long id) {
        return us.queryUserDs1(id);
    }
}</code>

The response correctly returns data from ds2 , confirming the interceptor works.

2.4 Underlying Mechanism of AbstractRoutingDataSource

<code>public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    private Map<Object, Object> targetDataSources;
    private Map<Object, DataSource> resolvedDataSources;
    public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); }
    protected DataSource determineTargetDataSource() {
        Object lookupKey = determineCurrentLookupKey();
        DataSource ds = this.resolvedDataSources.get(lookupKey);
        if (ds == null && (this.lenientFallback || lookupKey == null)) { ds = this.resolvedDefaultDataSource; }
        if (ds == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); }
        return ds;
    }
    @Override
    public void afterPropertiesSet() {
        this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
        this.targetDataSources.forEach((key, value) -> {
            Object lookupKey = resolveSpecifiedLookupKey(key);
            DataSource ds = resolveSpecifiedDataSource(value);
            this.resolvedDataSources.put(lookupKey, ds);
        });
        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }
    }
}</code>

The overall implementation is straightforward and demonstrates how to achieve dynamic multi‑datasource routing in a Spring Boot backend.

Hope this guide helps you master multi‑datasource switching.

JavaBackend DevelopmentSpring BootDynamic Routingmulti-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

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.