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