Definition
- Per-database beans – each database needs its own DataSource (connection pool), LocalContainerEntityManagerFactoryBean (JPA factory scanning that database's entity package), and PlatformTransactionManager.
- @Primary – marks one set of beans as the default for by-type injection; the other set is qualified by name.
- @EnableJpaRepositories – wires a package of repositories to a specific entityManagerFactoryRef and transactionManagerRef.
- Separate packages – each database's entities and repositories live in distinct packages so they never mix.
- Transaction scope – a single @Transactional spans one database; cross-database atomicity requires XA/JTA (two-phase commit) or a saga/outbox pattern.
National Bank of India keeps core accounts in one vault (the primary database) and audit/compliance records in a separate vault (the secondary database). Each vault has its own key (DataSource), its own filing clerk (EntityManagerFactory), and its own supervisor (TransactionManager). One supervisor's sign-off covers only their own vault; coordinating a change across both vaults needs a higher authority (XA) or a careful two-step plan (saga).
Understanding the Concepts
Why do you need manual configuration for two databases?
Spring Boot's auto-configuration assumes ONE database and wires one DataSource, one EntityManagerFactory, and one TransactionManager. The moment you need two, you take manual control of all three beans for each database. The pattern is symmetric: everything you define once for the single-database case, you now define twice.
# application.yml: two database connection configs
app:
datasource:
core:
url: jdbc:postgresql://db-host:5432/core_bank
username: core_user
password: secret
audit:
url: jdbc:postgresql://audit-host:5432/audit_bank
username: audit_user
password: secret
Single-database auto-config provides:
- 1 DataSource, 1 EntityManagerFactory, 1 TransactionManager
Two-database manual config provides:
- 2 DataSources, 2 EntityManagerFactories, 2 TransactionManagers
- Each wired to its own entity package and repository package
How do you define the DataSource beans?
Define one DataSource per database, binding connection properties from distinct config prefixes. Mark one as @Primary.
package com.bank.config;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("app.datasource.core")
public DataSourceProperties coreDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
public DataSource coreDataSource() {
return coreDataSourceProperties().initializeDataSourceBuilder().build();
}
@Bean
@ConfigurationProperties("app.datasource.audit")
public DataSourceProperties auditDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
public DataSource auditDataSource() {
return auditDataSourceProperties().initializeDataSourceBuilder().build();
}
}
How do you define the EntityManagerFactory beans?
Each EntityManagerFactory scans ONLY its own entity package. This prevents Hibernate from mapping audit entities against the core database.
package com.bank.config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import javax.sql.DataSource;
@Configuration
public class JpaConfig {
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean coreEntityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("coreDataSource") DataSource dataSource) {
return builder.dataSource(dataSource)
.packages("com.bank.core.model") // CORE entities only
.persistenceUnit("core")
.build();
}
@Bean
public LocalContainerEntityManagerFactoryBean auditEntityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("auditDataSource") DataSource dataSource) {
return builder.dataSource(dataSource)
.packages("com.bank.audit.model") // AUDIT entities only
.persistenceUnit("audit")
.build();
}
}
How do you define the TransactionManager beans?
Each TransactionManager is bound to its EntityManagerFactory. A @Transactional method resolves to one of these.
package com.bank.config;
import jakarta.persistence.EntityManagerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class TransactionConfig {
@Bean
@Primary
public PlatformTransactionManager coreTransactionManager(
@Qualifier("coreEntityManagerFactory") EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
@Bean
public PlatformTransactionManager auditTransactionManager(
@Qualifier("auditEntityManagerFactory") EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
}
How do you wire repositories to each database?
Each database's repositories live in a separate package and are wired with @EnableJpaRepositories pointing at the correct factory and transaction manager.
package com.bank.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@Configuration
@EnableJpaRepositories(
basePackages = "com.bank.core.repository",
entityManagerFactoryRef = "coreEntityManagerFactory",
transactionManagerRef = "coreTransactionManager")
class CoreJpaConfig {}
@Configuration
@EnableJpaRepositories(
basePackages = "com.bank.audit.repository",
entityManagerFactoryRef = "auditEntityManagerFactory",
transactionManagerRef = "auditTransactionManager")
class AuditJpaConfig {}
Package structure:
com.bank.core.model/ -> Account, Trade (core DB entities)
com.bank.core.repository/ -> AccountRepository (core DB repos)
com.bank.audit.model/ -> AuditLog (audit DB entities)
com.bank.audit.repository/ -> AuditLogRepository (audit DB repos)
What about cross-database transactions?
A single @Transactional commits against ONE transaction manager, hence ONE database. If a method writes to both databases, those are two independent local transactions. If the second fails after the first commits, the two systems are inconsistent.
package com.bank.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.bank.core.repository.AccountRepository;
import com.bank.audit.repository.AuditLogRepository;
import com.bank.core.model.Account;
import com.bank.audit.model.AuditLog;
@Service
public class TransferService {
private final AccountRepository accountRepository;
private final AuditLogRepository auditLogRepository;
public TransferService(AccountRepository accountRepository,
AuditLogRepository auditLogRepository) {
this.accountRepository = accountRepository;
this.auditLogRepository = auditLogRepository;
}
@Transactional // commits against CORE database only (@Primary)
public void transfer(String fromId, String toId, java.math.BigDecimal amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
from.debit(amount);
to.credit(amount);
// This commits to core DB
}
@Transactional("auditTransactionManager") // commits against AUDIT database
public void writeAuditLog(AuditLog log) {
auditLogRepository.save(log);
// This commits to audit DB
}
}
Cross-database risk:
- transfer() commits to core DB
- writeAuditLog() commits to audit DB
- If writeAuditLog() fails AFTER transfer() committed -> inconsistency
- Fix: XA/JTA for atomic cross-DB, or saga/outbox for eventual consistency
What are common mistakes?
The most common mistake is putting both databases' entities in the same package, causing Hibernate to try mapping audit entities against the core database. A second mistake is assuming @Transactional spans both databases.
// WRONG: entities in same package -> Hibernate maps ALL to one DB
com.bank.model/Account.java // core entity
com.bank.model/AuditLog.java // audit entity
// EntityManagerFactory scans com.bank.model -> maps BOTH to core DB!
// CORRECT: separate packages
com.bank.core.model/Account.java // scanned by core EMF
com.bank.audit.model/AuditLog.java // scanned by audit EMF
What should a senior engineer know?
A senior engineer disables or scopes DataSourceAutoConfiguration, configures Flyway/Liquibase per database, sizes connection pools independently, and designs for eventual consistency with a saga or outbox pattern instead of XA when possible. They explicitly specify the transaction manager on audit-writing methods.
// Senior pattern: explicit transaction manager on audit writes
@Transactional("auditTransactionManager")
public void writeAuditLog(AuditLog log) {
auditLogRepository.save(log);
}
// Outbox pattern: write audit event to core DB outbox table
// A separate process reads the outbox and writes to audit DB
// Avoids cross-DB transaction entirely
Visual Overview
Two-Database Configuration
============================================
Core Database Audit Database
+------------------------+ +------------------------+
| DataSource (core) | | DataSource (audit) |
| @Primary | | |
+----------+-------------+ +----------+-------------+
| |
v v
+------------------------+ +------------------------+
| EntityManagerFactory | | EntityManagerFactory |
| scans: core.model | | scans: audit.model |
| @Primary | | |
+----------+-------------+ +----------+-------------+
| |
v v
+------------------------+ +------------------------+
| TransactionManager | | TransactionManager |
| @Primary | | "auditTransactionMgr" |
+----------+-------------+ +----------+-------------+
| |
v v
+------------------------+ +------------------------+
| core.repository | | audit.repository |
| AccountRepository | | AuditLogRepository |
+------------------------+ +------------------------+
@Transactional = one DB only
Cross-DB atomicity = XA or Saga
Key Takeaway
To use two databases in one Spring Boot application, Rajesh gives up single-database auto-configuration and defines, for each database, three beans: a DataSource, a LocalContainerEntityManagerFactoryBean scanning only that database's entity package, and a PlatformTransactionManager. He keeps entities and repositories in separate packages and wires them with @EnableJpaRepositories. One set of beans is marked @Primary; the other is qualified by name.
At National Bank of India, core account data lives in the main transactional database and an immutable audit log lives in a separate compliance database. The service debits an account under the core transaction manager and writes an audit record under the audit transaction manager. Keeping the entities in separate packages is essential because otherwise Hibernate would try to map audit entities against the core database.
The most important correctness point Pankaj always raises is transaction scope. A plain @Transactional commits against exactly one transaction manager, hence one database. Cross-database writes are two independent local transactions, and if the second fails after the first commits, the systems are inconsistent. For atomicity, XA/JTA coordinates a two-phase commit, but in practice Pankaj designs for eventual consistency with a saga or outbox pattern.
Remember one sentence: two databases need two sets of DataSource, EntityManagerFactory, and TransactionManager beans in separate packages, and a single @Transactional is atomic within one database only.
---