How to Configure and Use Multiple DataSources in Spring Boot 3.2
Learn to configure multiple databases in Spring Boot 3.2 with JPA, HikariCP, and transaction management.
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
- ✓Java 17+ installed
- ✓Spring Boot 3.2 project with spring-boot-starter-data-jpa and spring-boot-starter-web
- ✓Two MySQL databases running (e.g., orders_db and reporting_db)
- ✓Basic understanding of JPA and Hibernate
• Define separate @Configuration classes for each DataSource, EntityManagerFactory, and TransactionManager using @Primary for one • Use spring.datasource. and custom prefixes (e.g., spring.datasource.orders.) in application.yml • Create separate @EnableJpaRepositories with distinct entityManagerFactoryRef and transactionManagerRef • Always mark one DataSource as @Primary to avoid ambiguity • Use @Transactional with explicit transactionManager name for cross-database operations
Think of your application as a hotel with two separate phone systems: one for guests (main database) and one for staff (reporting database). Each system has its own switchboard (DataSource), operator (EntityManagerFactory), and billing (TransactionManager). You need to configure each independently, but they share the same building (application context). The @Primary annotation tells Spring which one to use by default when not specified.
In production, you rarely get away with a single database. SaaS billing systems need one DB for transactional customer data and another for read-heavy analytics. Payment processors separate high-velocity transaction logs from slow-moving user profiles. Real-time analytics pipelines write to both a low-latency cache and a durable store. Spring Boot 3.2 makes this possible, but the official documentation glosses over the landmines. I've seen teams spend days debugging lazy initialization exceptions, transaction manager conflicts, and HikariCP connection leaks because they assumed @Primary was optional. This guide walks you through configuring two MySQL databases (one for orders, one for reporting) with JPA, HikariCP connection pooling, and proper transaction demarcation. We'll cover the exact configuration properties, Java config classes, and repository setups that work in production. By the end, you'll have a battle-tested pattern that handles schema migrations, connection failover, and cross-database transactions without sacrificing testability.
Setting Up the Project and Dependencies
Start with a Spring Boot 3.2 project. Add these dependencies to your build.gradle (or pom.xml). For this tutorial, we'll use MySQL, but the pattern works for PostgreSQL, Oracle, or any JPA-supported database. We'll also include HikariCP (Spring Boot's default connection pool) and Flyway for schema migrations across both datasources. In production, you'll want Micrometer for connection pool metrics. Here's the build.gradle:
What the Official Docs Won't Tell You
Spring Boot's auto-configuration for DataSource is designed for a single database. When you define a second DataSource bean, the auto-configuration back offs entirely for both—you lose all the nice defaults like HikariCP tuning, connection validation, and metrics. You must manually configure everything. Another hidden pitfall: @EnableJpaRepositories scans packages based on the entityManagerFactoryRef. If you accidentally put an entity class in the wrong package, Spring will try to create a table in the wrong database, leading to cryptic 'Table not found' errors at runtime. Also, the @Primary annotation is not optional. Without it, Spring will throw a NoUniqueBeanDefinitionException when injecting EntityManager or PlatformTransactionManager. The official docs imply you can skip @Primary if you qualify all injections, but in practice, many internal Spring components (like OpenEntityManagerInViewFilter) expect a single primary. Finally, transaction management across datasources requires careful use of @Transactional with explicit transactionManager name. Spring's default transaction propagation (REQUIRED) will join the primary transaction if you don't specify which manager to use. This caused a production incident where writes to the secondary datasource were silently rolled back because they joined the primary transaction that only flushed to the primary database.
Configuring the Primary DataSource (Orders)
Create a configuration class for the orders database. This will be your primary datasource handling transactional customer data. We'll define beans for DataSource, EntityManagerFactory, and PlatformTransactionManager. The @Primary annotation ensures Spring uses this configuration by default when injecting EntityManager or TransactionManager without a qualifier. Notice we use @ConfigurationProperties with the 'spring.datasource.orders' prefix to bind our custom properties. The EntityManagerFactory uses Hibernate's LocalContainerEntityManagerFactoryBean with a custom persistence unit name. We set packagesToScan to the package containing Order entities (e.g., com.example.orders.entity). The TransactionManager is a JpaTransactionManager wrapping the EntityManagerFactory. We'll also configure Flyway for this datasource by creating a dedicated FlywayConfiguration class later.
Configuring the Secondary DataSource (Reporting)
Now configure the reporting datasource. This is identical in structure to the primary but without @Primary. The key differences: use a different @ConfigurationProperties prefix ('spring.datasource.reporting'), a different persistence unit name ('reporting'), and a different transaction manager bean name. The @EnableJpaRepositories annotation points to the reporting repository package and references the correct entityManagerFactoryRef and transactionManagerRef. Note that we use @Qualifier extensively to avoid bean name conflicts. In production, you might want a smaller connection pool for reporting since it's read-heavy. We'll also configure a separate Flyway migration for this datasource. The reporting database likely has different schema evolution needs (e.g., denormalized tables for analytics).
Writing Repositories and Entities
Create separate packages for entities and repositories of each datasource. For the orders database, create an Order entity with JPA annotations and an OrderRepository extending JpaRepository. For the reporting database, create a SalesSummary entity and SalesSummaryRepository. The repository interfaces are standard Spring Data JPA. The critical part is that they reside in the packages specified in the @EnableJpaRepositories annotations. If you accidentally put OrderRepository in the reporting repository package, Spring will try to connect it to the reporting datasource, causing a 'Table not found' error because the orders table doesn't exist in reporting_db. In production, we use a naming convention: com.example.{datasource}.entity and com.example.{datasource}.repository. This makes it obvious which datasource each class belongs to.
Transaction Management Across Multiple Datasources
When a service method needs to write to both databases in a single transaction, you have two options: use distributed transactions (XA) or handle it manually with compensation logic. XA transactions are supported via JTA (e.g., Atomikos), but they introduce complexity and performance overhead. In most production scenarios, we avoid XA and instead use a pattern: write to the primary database first, then write to the secondary. If the secondary write fails, compensate by rolling back the primary write. This is called the 'Saga pattern'. For this tutorial, we'll show the simpler case: separate transactions for each datasource. Use @Transactional with the specific transactionManager name. Note that @Transactional on a service method without specifying transactionManager will use the primary (orders) transaction manager. To use the reporting transaction manager, annotate with @Transactional("reportingTransactionManager").
Testing Multiple Datasources with Testcontainers
Testing multiple datasources locally is a nightmare if you rely on embedded databases like H2. H2's SQL dialect differs from MySQL, and you'll miss production bugs. Use Testcontainers to spin up real MySQL containers for each datasource. In your test configuration, override the datasource properties with Testcontainers' JDBC URLs. Create a test configuration class that provides two MySQL containers, one for each database. Use @DynamicPropertySource to set the spring.datasource.* properties dynamically. This ensures your tests run against the same database version as production. We also recommend using @Sql annotations to load test data into specific databases by specifying the DataSource bean name.
Production Monitoring and Debugging
Once your multiple datasource setup is in production, you need to monitor connection pool health, transaction commit rates, and slow queries per datasource. Spring Boot Actuator exposes HikariCP metrics via Micrometer. Add 'management.endpoints.web.exposure.include=health,metrics,prometheus' to expose them. For each datasource, you'll see metrics like 'hikaricp.connections.active', 'hikaricp.connections.pending', and 'hikaricp.connections.timeout'. Create custom Grafana dashboards per datasource. For transaction debugging, enable Hibernate statistics per datasource by setting 'spring.jpa.properties.hibernate.generate_statistics=true'. This logs transaction counts, flushes, and L2 cache hits per persistence unit. In production, we also add a custom health indicator that pings both databases and reports their status separately. The following code shows a health indicator and a debug endpoint that lists all configured datasources.
The Case of the Vanishing Transaction
- Always name your transaction manager beans explicitly—never rely on defaults across major Spring Boot versions.
- Add integration tests that verify writes to all datasources in a single transaction.
- Monitor transaction commit counts per datasource in production (Micrometer + Prometheus).
grep -r "@Primary" src/main/java/grep -r "DataSource\|EntityManagerFactory\|PlatformTransactionManager" src/main/java/| File | Command / Code | Purpose |
|---|---|---|
| build.gradle | dependencies { | Setting Up the Project and Dependencies |
| application.yml | spring: | What the Official Docs Won't Tell You |
| OrdersDataSourceConfig.java | @Configuration | Configuring the Primary DataSource (Orders) |
| ReportingDataSourceConfig.java | @Configuration | Configuring the Secondary DataSource (Reporting) |
| OrderRepository.java | @Repository | Writing Repositories and Entities |
| OrderService.java | @Service | Transaction Management Across Multiple Datasources |
| MultipleDataSourceTest.java | @SpringBootTest | Testing Multiple Datasources with Testcontainers |
| DataSourceHealthIndicator.java | @Component | Production Monitoring and Debugging |
Key takeaways
Interview Questions on This Topic
How do you configure multiple datasources in Spring Boot and what is the role of @Primary?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
That's Spring Boot. Mark it forged?
4 min read · try the examples if you haven't