Working with Multiple Data Sources
1. Configuring Primary Data Source
Example: Primary and secondary datasource config
app:
datasource:
primary:
url: jdbc:postgresql://localhost:5432/main
username: app
password: secret
secondary:
url: jdbc:mysql://localhost:3306/reports
username: rpt
password: secret
2. Configuring Secondary Data Source
Example: Multiple DataSource beans
@Configuration
public class DataSourceConfig {
@Bean @Primary
@ConfigurationProperties("app.datasource.primary")
DataSourceProperties primaryProps() { return new DataSourceProperties(); }
@Bean @Primary
DataSource primaryDs(@Qualifier("primaryProps") DataSourceProperties p) {
return p.initializeDataSourceBuilder().build();
}
@Bean
@ConfigurationProperties("app.datasource.secondary")
DataSourceProperties secondaryProps() { return new DataSourceProperties(); }
@Bean
DataSource secondaryDs(@Qualifier("secondaryProps") DataSourceProperties p) {
return p.initializeDataSourceBuilder().build();
}
}
3. Creating Multiple EntityManagerFactory Beans
Example: Multiple EntityManagerFactory beans
@Bean @Primary
LocalContainerEntityManagerFactoryBean primaryEmf(
EntityManagerFactoryBuilder b, @Qualifier("primaryDs") DataSource ds) {
return b.dataSource(ds).packages("com.example.main").persistenceUnit("main").build();
}
@Bean
LocalContainerEntityManagerFactoryBean secondaryEmf(
EntityManagerFactoryBuilder b, @Qualifier("secondaryDs") DataSource ds) {
return b.dataSource(ds).packages("com.example.report").persistenceUnit("report").build();
}
4. Creating Multiple TransactionManager Beans
Example: Multiple transaction managers
@Bean @Primary
PlatformTransactionManager primaryTx(@Qualifier("primaryEmf") EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
@Bean
PlatformTransactionManager secondaryTx(@Qualifier("secondaryEmf") EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
5. Separating Entity Packages by Data Source
| Package |
Bound To |
com.example.main |
Primary EMF/repos |
com.example.report |
Secondary EMF/repos |
6. Using @Primary for Default Data Source
Note: Mark exactly ONE bean of each type (DataSource, EntityManagerFactory,
TransactionManager) as @Primary — used when no qualifier is specified.
7. Configuring JPA Repositories for Each Data Source
Example: Route repositories to separate data sources
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.main.repo",
entityManagerFactoryRef = "primaryEmf",
transactionManagerRef = "primaryTx")
public class PrimaryJpaConfig {}
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.report.repo",
entityManagerFactoryRef = "secondaryEmf",
transactionManagerRef = "secondaryTx")
public class SecondaryJpaConfig {}
8. Handling Transactions Across Data Sources
| Approach |
Note |
ChainedTransactionManager DEPRECATED |
Best-effort — not true 2PC |
| JTA + Atomikos/Narayana |
True XA two-phase commit |
| Outbox pattern |
Eventually consistent (preferred) |
9. Using Routing Data Sources (AbstractRoutingDataSource)
Example: Routing DataSource by ThreadLocal key
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override protected Object determineCurrentLookupKey() {
return DbContext.get(); // ThreadLocal: "primary" / "replica"
}
}
@Bean
DataSource routingDs(@Qualifier("primaryDs") DataSource a, @Qualifier("secondaryDs") DataSource b) {
RoutingDataSource ds = new RoutingDataSource();
ds.setTargetDataSources(Map.of("primary", a, "replica", b));
ds.setDefaultTargetDataSource(a);
return ds;
}
10. Testing Multiple Data Sources
| Tool |
Use |
| Two Testcontainers |
One per DataSource |
@DynamicPropertySource |
Inject both URLs |
@DataJpaTest |
Not ideal — auto-configures only one |
@SpringBootTest |
Preferred — loads full config |