Configure and Use Multiple DataSources in Spring Boot
1. Introduction
Spring Boot has become the de facto framework for building modern Java applications, thanks to its convention-over-configuration approach and robust ecosystem. One common requirement in enterprise applications is the need to interact with multiple databases—whether for separating concerns, integrating legacy systems, or handling distributed data. Configuring and using multiple DataSources in Spring Boot allows developers to seamlessly connect to different databases within the same application.
In this blog post, we’ll explore how to configure multiple DataSources in a Spring Boot application, provide a working example, and discuss real-world use cases. Whether you’re dealing with microservices, reporting systems, or multi-tenant architectures, mastering this technique will enhance your Spring Boot expertise. Let’s dive in and see how to set this up effectively as of February 25, 2025, with Spring Boot 3.2.0.
![]() |
Configure and Use Multiple DataSources in Spring Boot |
2. Usages
The ability to configure multiple DataSources in Spring Boot is a game-changer for various scenarios. Here are some real-time use cases where this feature shines:
- Multi-Tenant Applications: In a SaaS platform, each tenant might have its own database for data isolation. Multiple DataSources allow you to route requests to the appropriate tenant database dynamically.
- Separation of Concerns: Separate transactional data (e.g., orders, users) from analytical or reporting data to optimize performance and maintainability.
- Legacy System Integration: Connect to an existing legacy database while migrating data to a new system, enabling a phased transition.
- Microservices with Shared Databases: In a microservices architecture, a service might need to access a primary database and a secondary one for specific operations like caching or auditing.
- Read-Write Splitting: Use one DataSource for write operations (master database) and another for read operations (replica database) to distribute load effectively.
These use cases demonstrate the flexibility of multiple DataSources, making it a critical skill for enterprise-grade Spring Boot development.
3. Code Example
Let’s walk through a practical example of configuring two DataSources in a Spring Boot application—one for a "primary" database (e.g., user data) and another for a "secondary" database (e.g., order data). We’ll use H2 in-memory databases for simplicity, but the approach applies to any database like MySQL, PostgreSQL, or Oracle.
Step 1: Add Dependencies
In your pom.xml
, include the necessary dependencies for Spring Data JPA and H2:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
<scope>runtime</scope>
</dependency>
</dependencies>
Step 2: Configure DataSources in application.properties
Define the properties for both DataSources in src/main/resources/application.properties
:
# Primary DataSource (Users)
spring.datasource.primary.url=jdbc:h2:mem:primarydb
spring.datasource.primary.username=sa
spring.datasource.primary.password=
spring.datasource.primary.driver-class-name=org.h2.Driver
# Secondary DataSource (Orders)
spring.datasource.secondary.url=jdbc:h2:mem:secondarydb
spring.datasource.secondary.username=sa
spring.datasource.secondary.password=
spring.datasource.secondary.driver-class-name=org.h2.Driver
# JPA settings (optional for this example)
spring.jpa.hibernate.ddl-auto=update
Step 3: Create DataSource Configuration Classes
Set up two configuration classes—one for each DataSource—using @Configuration
and @EnableJpaRepositories
to separate the JPA repositories and entity managers.
Primary DataSource Configuration:
package com.example.demo.config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.demo.repository.primary",
entityManagerFactoryRef = "primaryEntityManagerFactory",
transactionManagerRef = "primaryTransactionManager"
)
public class PrimaryDataSourceConfig {
@Primary
@Bean(name = "primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Primary
@Bean(name = "primaryEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
@Qualifier("primaryDataSource") DataSource dataSource) {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.example.demo.entity.primary");
em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
return em;
}
@Primary
@Bean(name = "primaryTransactionManager")
public PlatformTransactionManager primaryTransactionManager(
@Qualifier("primaryEntityManagerFactory") LocalContainerEntityManagerFactoryBean emf) {
return new JpaTransactionManager(emf.getObject());
}
}
Secondary DataSource Configuration:
package com.example.demo.config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.demo.repository.secondary",
entityManagerFactoryRef = "secondaryEntityManagerFactory",
transactionManagerRef = "secondaryTransactionManager"
)
public class SecondaryDataSourceConfig {
@Bean(name = "secondaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "secondaryEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory(
@Qualifier("secondaryDataSource") DataSource dataSource) {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.example.demo.entity.secondary");
em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
return em;
}
@Bean(name = "secondaryTransactionManager")
public PlatformTransactionManager secondaryTransactionManager(
@Qualifier("secondaryEntityManagerFactory") LocalContainerEntityManagerFactoryBean emf) {
return new JpaTransactionManager(emf.getObject());
}
}
Step 4: Define Entities and Repositories
Create entities and repositories for each DataSource.
Primary Entity (User):
package com.example.demo.entity.primary;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
private Long id;
private String name;
// Getters, setters, and constructors
public User() {}
public User(Long id, String name) {
this.id = id;
this.name = name;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
Primary Repository:
package com.example.demo.repository.primary;
import com.example.demo.entity.primary.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
Secondary Entity (Order):
package com.example.demo.entity.secondary;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
public class Order {
@Id
private Long id;
private String product;
// Getters, setters, and constructors
public Order() {}
public Order(Long id, String product) {
this.id = id;
this.product = product;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getProduct() { return product; }
public void setProduct(String product) { this.product = product; }
}
Secondary Repository:
package com.example.demo.repository.secondary;
import com.example.demo.entity.secondary.Order;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OrderRepository extends JpaRepository<Order, Long> {
}
Step 5: Test the Configuration
Create a simple test class to verify the setup:
package com.example.demo;
import com.example.demo.entity.primary.User;
import com.example.demo.entity.secondary.Order;
import com.example.demo.repository.primary.UserRepository;
import com.example.demo.repository.secondary.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MultipleDataSourcesApplication implements CommandLineRunner {
@Autowired
private UserRepository userRepository;
@Autowired
private OrderRepository orderRepository;
public static void main(String[] args) {
SpringApplication.run(MultipleDataSourcesApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
userRepository.save(new User(1L, "John Doe"));
orderRepository.save(new Order(1L, "Laptop"));
System.out.println("Users: " + userRepository.findAll());
System.out.println("Orders: " + orderRepository.findAll());
}
}
Run the application. You’ll see the data saved and retrieved from both DataSources, confirming the configuration works.
4. Best Practices
To ensure your multiple DataSource setup is robust and maintainable, follow these best practices:
- Use
@Primary
Wisely: Mark one DataSource as@Primary
to avoid ambiguity when Spring Boot needs a default DataSource (e.g., for non-qualified injections). - Separate Packages: Keep entities, repositories, and configurations for each DataSource in distinct packages to avoid classpath scanning issues.
- Transaction Management: Define separate transaction managers for each DataSource to handle transactions independently.
- Dynamic Routing: For multi-tenant scenarios, consider using
AbstractRoutingDataSource
to switch DataSources dynamically based on runtime conditions (e.g., tenant ID). - Test Thoroughly: Validate CRUD operations for each DataSource to ensure isolation and correct configuration.
- Monitor Performance: When using multiple databases, monitor connection pools and latency to optimize resource usage.
5. Conclusion
Configuring and using multiple DataSources in Spring Boot opens up a world of possibilities for complex applications. Whether you’re building a multi-tenant SaaS platform, integrating legacy systems, or splitting read-write operations, this approach provides the flexibility and scalability you need. The working example above demonstrates how to set up two DataSources with JPA, and the best practices ensure your implementation is production-ready.
As of February 25, 2025, this technique remains a cornerstone of advanced Spring Boot development. Try it out in your next project, and adapt it to your specific use case—whether it’s for reporting, auditing, or tenant isolation. Have questions or a unique scenario? Drop a comment below—I’d love to hear from you!