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:

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

Routing datasource extending AbstractRoutingDataSource:

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

Properties class inheriting from Spring Boot's DataSourceProperties:

public class PackDataSourceProperties extends DataSourceProperties { }

Configuration holder for all data sources:

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

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

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

Custom Annotation and Aspect

Annotation to specify a datasource:

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

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

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

YAML Configuration

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

Testing with JPA

Entity definition:

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

Repository and service using the @Ds annotation:

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

Test cases:

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

2.3 Controlling via Web Request

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

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

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

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

Example controller that uses the service:

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

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

2.4 Underlying Mechanism of AbstractRoutingDataSource

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

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.

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.

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

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.