Spring Boot
Spring Data JPA
Multiple DataSources
Java Development
Database Integration

Spring Boot, Spring Data JPA with multiple DataSources

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Using multiple JPA data sources in Spring Boot means you must be explicit about which entities, repositories, entity managers, and transaction managers belong to which database. Spring Boot can auto-configure one data source very conveniently, but once you add a second one, clarity and separation become more important than convenience.

Define Separate Properties

Start by giving each database its own configuration prefix.

properties
1app.datasource.primary.url=jdbc:postgresql://localhost:5432/appdb
2app.datasource.primary.username=app
3app.datasource.primary.password=secret
4
5app.datasource.reporting.url=jdbc:mysql://localhost:3306/reporting
6app.datasource.reporting.username=report
7app.datasource.reporting.password=secret

Keeping the settings separate avoids ambiguity and makes it obvious which database each block configures.

Primary Data Source Configuration

Here is a typical primary configuration:

java
1package com.example.config;
2
3import javax.sql.DataSource;
4import org.springframework.beans.factory.annotation.Qualifier;
5import org.springframework.boot.context.properties.ConfigurationProperties;
6import org.springframework.boot.jdbc.DataSourceBuilder;
7import org.springframework.context.annotation.Bean;
8import org.springframework.context.annotation.Configuration;
9import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
10import org.springframework.orm.jpa.JpaTransactionManager;
11import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
12import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
13import org.springframework.transaction.PlatformTransactionManager;
14
15@Configuration
16@EnableJpaRepositories(
17    basePackages = "com.example.primary.repo",
18    entityManagerFactoryRef = "primaryEntityManagerFactory",
19    transactionManagerRef = "primaryTransactionManager"
20)
21public class PrimaryDbConfig {
22
23    @Bean
24    @ConfigurationProperties("app.datasource.primary")
25    public DataSource primaryDataSource() {
26        return DataSourceBuilder.create().build();
27    }
28
29    @Bean
30    public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
31            @Qualifier("primaryDataSource") DataSource dataSource) {
32        LocalContainerEntityManagerFactoryBean bean = new LocalContainerEntityManagerFactoryBean();
33        bean.setDataSource(dataSource);
34        bean.setPackagesToScan("com.example.primary.model");
35        bean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
36        return bean;
37    }
38
39    @Bean
40    public PlatformTransactionManager primaryTransactionManager(
41            @Qualifier("primaryEntityManagerFactory")
42            LocalContainerEntityManagerFactoryBean emf) {
43        return new JpaTransactionManager(emf.getObject());
44    }
45}

The important part is that repositories in com.example.primary.repo are tied explicitly to the primary entity manager and transaction manager.

Secondary Data Source Configuration

The second configuration follows the same pattern with different package boundaries:

java
1@Configuration
2@EnableJpaRepositories(
3    basePackages = "com.example.reporting.repo",
4    entityManagerFactoryRef = "reportingEntityManagerFactory",
5    transactionManagerRef = "reportingTransactionManager"
6)
7public class ReportingDbConfig {
8
9    @Bean
10    @ConfigurationProperties("app.datasource.reporting")
11    public DataSource reportingDataSource() {
12        return DataSourceBuilder.create().build();
13    }
14
15    @Bean
16    public LocalContainerEntityManagerFactoryBean reportingEntityManagerFactory(
17            @Qualifier("reportingDataSource") DataSource dataSource) {
18        LocalContainerEntityManagerFactoryBean bean = new LocalContainerEntityManagerFactoryBean();
19        bean.setDataSource(dataSource);
20        bean.setPackagesToScan("com.example.reporting.model");
21        bean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
22        return bean;
23    }
24
25    @Bean
26    public PlatformTransactionManager reportingTransactionManager(
27            @Qualifier("reportingEntityManagerFactory")
28            LocalContainerEntityManagerFactoryBean emf) {
29        return new JpaTransactionManager(emf.getObject());
30    }
31}

This separation is what keeps Spring from mixing entities and repositories across the two databases.

Why Package Boundaries Matter

Multiple data sources become manageable when you organize code like this:

  • 'com.example.primary.model'
  • 'com.example.primary.repo'
  • 'com.example.reporting.model'
  • 'com.example.reporting.repo'

Once those boundaries are clear, the annotations can bind each repository set to the correct database components.

Mark One Data Source as Primary When Needed

In many real applications, one data source is the default for framework integrations that expect a single bean. In that case, mark the main data source and its related beans with @Primary so generic injections resolve predictably.

That does not remove the need for qualifiers elsewhere, but it reduces ambiguity for the application's default database path.

Common Pitfalls

The most common mistake is putting repositories for both databases under the same package tree and expecting Spring to infer the right data source automatically. With multiple JPA contexts, explicit package separation is much safer.

Another issue is forgetting separate transaction managers. If both databases share a vague transaction setup, repository operations may fail or bind to the wrong persistence context.

A third pitfall is assuming distributed transactions are automatic. If one service method writes to two different databases, you need to think carefully about consistency and transaction strategy.

Finally, resist the temptation to hide all of this behind copy-pasted config without naming conventions. Multiple data source setups are maintainable only when the naming is consistent and obvious.

Summary

  • Multiple JPA data sources in Spring Boot require explicit configuration.
  • Give each database its own data source, entity manager factory, transaction manager, and repository package.
  • Keep entity and repository packages clearly separated per database.
  • Do not rely on one-database Spring Boot defaults once a second data source is introduced.
  • Treat cross-database transaction design as a separate architectural concern.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.