Back to Multiple DataSources
Q1Question 1 of 2

How does @Transactional work with multiple DataSources?

Key Concepts
  • Each DataSource needs its own PlatformTransactionManager because a transaction manager wraps exactly one connection source
  • Select the manager by name with @Transactional("secondaryTxManager") or use @Primary for the default
  • A single @Transactional cannot atomically coordinate two independent databases because it manages one connection
  • Writing to two databases inside one annotated method creates two separate transactions on two separate connections
  • JTA with XA two-phase commit can coordinate two databases atomically but is heavy and blocking
  • For microservices, use the Transactional Outbox pattern: write business row plus outbox event in one local TX, relay asynchronously

Definition

Each DataSource needs its own PlatformTransactionManager, selected by name in @Transactional, and a single @Transactional cannot atomically span two independent databases without JTA/XA.
  • PlatformTransactionManager per DataSource -- a transaction manager wraps exactly one connection source, so each DataSource must have its own manager bean.
  • @Transactional("secondaryTxManager") -- the annotation's value or transactionManager attribute selects which manager to use; a plain @Transactional uses the @Primary manager.
  • Single-connection limit -- a single @Transactional manages one connection, so writing to two databases inside one method creates two separate transactions on two separate connections.
  • JTA/XA -- the Java Transaction API with XA two-phase commit can coordinate two databases atomically, but it is heavy, blocking, and discourages in microservices.
  • Transactional Outbox -- write the business row and an outbox event in the same single-database transaction, then relay the event asynchronously to the second system.

At the National Bank of India, Rajesh has two separate vaults: one for savings accounts and one for the loan division. Each vault has its own manager. A single vault manager can only open and close one vault at a time. If Rajesh needs to move money from savings to a loan account in the other vault, one manager cannot lock both vaults simultaneously. He either needs a senior coordinator (JTA) who locks both vaults together, or he processes one vault at a time and uses a messenger (outbox) to relay the second step.

Understanding the Concepts

How does @Transactional work with multiple DataSources?

When an application talks to more than one database, each DataSource must have its own PlatformTransactionManager. A transaction manager wraps exactly one connection source. You pick which manager a method uses by name: @Transactional("loanTxManager") runs against the loan database, while a plain @Transactional uses the @Primary manager.

Spring binds the chosen manager's connection to the thread for the duration of the method, so all repositories wired to that DataSource join the same transaction.

JAVA
package com.bank.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.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

@Configuration
public class MultiDataSourceConfig {

    @Primary
    @Bean
    @ConfigurationProperties("spring.datasource.savings")
    public DataSource savingsDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.loan")
    public DataSource loanDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean
    public PlatformTransactionManager savingsTxManager(
            @Qualifier("savingsEntityManagerFactory")
            EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }

    @Bean
    public PlatformTransactionManager loanTxManager(
            @Qualifier("loanEntityManagerFactory")
            EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}
YAML
# application.yml
spring:
  datasource:
    savings:
      url: jdbc:postgresql://db1:5432/savings_db
      username: savings_user
    loan:
      url: jdbc:postgresql://db2:5432/loan_db
      username: loan_user

Why can a single @Transactional not span two databases?

A @Transactional method acquires a connection from the transaction manager's DataSource and binds it to the thread. All repository calls wired to that DataSource use the same connection. But repositories wired to a different DataSource will either get their own unmanaged connection or fail, because the thread-bound connection belongs to a different DataSource.

If you write to both databases inside one annotated method, you are silently running two independent transactions. The primary commits first. If the secondary then fails, the primary is already committed and you have an inconsistent state.

JAVA
package com.bank.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Service
public class CrossDatabaseServiceBroken {

    private final SavingsAccountRepository savingsRepo; // savings DB
    private final LoanAccountRepository loanRepo;       // loan DB

    public CrossDatabaseServiceBroken(SavingsAccountRepository savingsRepo,
                                       LoanAccountRepository loanRepo) {
        this.savingsRepo = savingsRepo;
        this.loanRepo = loanRepo;
    }

    // WRONG: single @Transactional, two databases
    @Transactional  // uses @Primary (savings) manager
    public void transferToLoan(String savingsId, String loanId,
                                BigDecimal amount) {
        savingsRepo.debit(savingsId, amount);  // savings TX
        loanRepo.credit(loanId, amount);       // NOT in savings TX!
        // If loanRepo.credit() fails, savingsRepo.debit() is
        // already committed -- inconsistent state
    }
}
TEXT
What actually happens:
  1. Spring begins TX on savings DataSource
  2. savingsRepo.debit() -- uses savings connection (managed)
  3. loanRepo.credit() -- uses loan DataSource (UNMANAGED)
  4. On success: savings TX commits, loan write committed separately
  5. If loan fails after savings commits: INCONSISTENT

What is the correct approach with separate transaction managers?

The correct approach is to use @Transactional with the specific manager name for each database operation. This makes it explicit that each operation is in its own transaction.

JAVA
package com.bank.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Service
public class CrossDatabaseServiceExplicit {

    private final SavingsAccountRepository savingsRepo;
    private final LoanAccountRepository loanRepo;

    public CrossDatabaseServiceExplicit(
            SavingsAccountRepository savingsRepo,
            LoanAccountRepository loanRepo) {
        this.savingsRepo = savingsRepo;
        this.loanRepo = loanRepo;
    }

    // Explicit: each DB gets its own transaction
    public void transferToLoan(String savingsId, String loanId,
                                BigDecimal amount) {
        debitSavings(savingsId, amount);  // TX 1
        creditLoan(loanId, amount);       // TX 2
    }

    @Transactional  // uses @Primary savings manager
    public void debitSavings(String savingsId, BigDecimal amount) {
        savingsRepo.debit(savingsId, amount);
    }

    @Transactional("loanTxManager")
    public void creditLoan(String loanId, BigDecimal amount) {
        loanRepo.credit(loanId, amount);
    }
}

How does JTA/XA solve cross-database atomicity?

JTA (Java Transaction API) with XA two-phase commit can coordinate multiple databases atomically. An XA-capable DataSource supports the XA protocol, which lets an external transaction manager (like Atomikos or Narayana) manage its commit. A single @Transactional method can then write to two XA databases and have them commit together.

The cost is high: between the prepare and commit phases, both databases hold locks across network round-trips, which kills throughput under load. The coordinator is also a single point of failure.

JAVA
package com.bank.config;

import com.atomikos.jdbc.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.jta.JtaTransactionManager;

@Configuration
public class JtaConfig {

    @Bean
    public AtomikosDataSourceBean savingsXaDataSource() {
        AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
        ds.setUniqueResourceName("savingsXa");
        ds.setXaDataSourceClassName(
            "org.postgresql.xa.PGXADataSource");
        return ds;
    }

    @Bean
    public AtomikosDataSourceBean loanXaDataSource() {
        AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
        ds.setUniqueResourceName("loanXa");
        ds.setXaDataSourceClassName(
            "org.postgresql.xa.PGXADataSource");
        return ds;
    }

    @Bean
    public JtaTransactionManager transactionManager() {
        return new JtaTransactionManager();  // Atomikos auto-detected
    }
}

What is the Transactional Outbox alternative?

For microservices, the preferred approach avoids distributed transactions entirely. Write the business row and an outbox event in the same single-database transaction. A separate relay process then reads unsent outbox rows and publishes them to Kafka or calls the second service. This guarantees that the event is published if and only if the business data committed.

JAVA
package com.bank.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Service
public class OutboxTransferService {

    private final SavingsAccountRepository savingsRepo;
    private final OutboxRepository outboxRepo;

    public OutboxTransferService(SavingsAccountRepository savingsRepo,
                                  OutboxRepository outboxRepo) {
        this.savingsRepo = savingsRepo;
        this.outboxRepo = outboxRepo;
    }

    @Transactional  // single DB transaction
    public void transferToLoan(String savingsId, String loanId,
                                BigDecimal amount) {
        savingsRepo.debit(savingsId, amount);

        // Outbox row in the SAME transaction
        outboxRepo.save(new OutboxEvent(
            "LOAN_CREDIT",
            "{\"loanId\":\"" + loanId
                + "\",\"amount\":" + amount + "}"));
        // Both committed atomically -- no distributed TX needed
    }
}
TEXT
Outbox flow:
  1. debit savings + insert outbox row -- ONE local TX, atomic
  2. Relay process reads outbox rows, publishes to Kafka
  3. Loan service consumes Kafka message, credits loan account
  4. Eventual consistency achieved without JTA/XA

What are common mistakes?

The most common mistake is assuming that a single @Transactional method automatically coordinates two DataSources. Developers write to both databases inside one annotated method and discover inconsistencies in production.

JAVA
// WRONG: assumes single @Transactional spans both databases
@Transactional
public void crossDatabaseWrite() {
    savingsRepo.debit("SAV2001", new BigDecimal("50000"));
    loanRepo.credit("LOAN001", new BigDecimal("50000"));
    // These are TWO independent transactions!
}

// CORRECT: use outbox or explicit separate transactions
public void crossDatabaseWriteFixed() {
    debitSavingsWithOutbox("SAV2001", "LOAN001",
        new BigDecimal("50000"));
}

@Transactional  // single savings DB transaction
public void debitSavingsWithOutbox(String savingsId,
        String loanId, BigDecimal amount) {
    savingsRepo.debit(savingsId, amount);
    outboxRepo.save(new OutboxEvent("LOAN_CREDIT",
        loanId + ":" + amount));
}

What should a senior engineer know?

A senior engineer knows that Spring Boot auto-configures only one PlatformTransactionManager by default. Adding a second DataSource requires explicit configuration of both managers, entity manager factories, and repository scan packages with @EnableJpaRepositories(basePackages = ..., entityManagerFactoryRef = ..., transactionManagerRef = ...).

Senior engineers also know about ChainedTransactionManager, which commits multiple managers in sequence but provides no atomicity guarantee: if the second commit fails after the first succeeds, you are inconsistent. It is a best-effort approach, not a replacement for JTA.

JAVA
package com.bank.config;

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableJpaRepositories(
    basePackages = "com.bank.repository.savings",
    entityManagerFactoryRef = "savingsEntityManagerFactory",
    transactionManagerRef = "savingsTxManager"
)
public class SavingsJpaConfig {
}

@Configuration
@EnableJpaRepositories(
    basePackages = "com.bank.repository.loan",
    entityManagerFactoryRef = "loanEntityManagerFactory",
    transactionManagerRef = "loanTxManager"
)
public class LoanJpaConfig {
}

What are common interview follow-up questions?

  • How do you configure two EntityManagerFactory beans for two DataSources in Spring Boot?
  • What happens if you forget to specify the transaction manager name in @Transactional?
  • Is ChainedTransactionManager a valid alternative to JTA for cross-database writes?
  • How does the Transactional Outbox pattern handle failures in the relay process?
  • Can you mix JPA and JDBC DataSources in the same application with separate transaction managers?

Visual Overview

DIAGRAM
  ┌───────────────────────────────────────────────────┐
  │            Multiple DataSources Setup              │
  │                                                    │
  │  Savings DB                    Loan DB             │
  │  ┌─────────────────┐          ┌─────────────────┐ │
  │  │ savingsDataSource│          │ loanDataSource  │ │
  │  └────────┬────────┘          └────────┬────────┘ │
  │           │                            │           │
  │  ┌────────▼────────┐          ┌────────▼────────┐ │
  │  │ savingsTxManager│          │ loanTxManager   │ │
  │  │ (@Primary)      │          │                 │ │
  │  └────────┬────────┘          └────────┬────────┘ │
  │           │                            │           │
  │  @Transactional         @Transactional("loanTxManager")
  │           │                            │           │
  │  savingsRepo.debit()    loanRepo.credit()          │
  └───────────────────────────────────────────────────┘

  Single @Transactional CANNOT span both:
  ┌─────────────────────────────────────────┐
  │ @Transactional (savings manager)        │
  │  savingsRepo.debit() -- MANAGED         │
  │  loanRepo.credit()   -- UNMANAGED!      │
  │  If loan fails after savings commits:   │
  │  INCONSISTENT STATE                     │
  └─────────────────────────────────────────┘

Key Takeaway

With multiple DataSources, each database needs its own PlatformTransactionManager because a transaction manager wraps exactly one connection source. You select which manager a method uses by name: a plain @Transactional uses the @Primary manager, while @Transactional("loanTxManager") runs against the loan database. Spring binds the chosen manager's connection to the thread, so repositories wired to that DataSource join the same transaction.

The crucial limitation is that a single @Transactional cannot atomically coordinate two independent databases. Writing to both savings and loan databases inside one annotated method creates two separate transactions on two separate connections. If the second fails after the first commits, the databases diverge. JTA with XA two-phase commit can solve this, but it holds locks across network round-trips and the coordinator is a single point of failure.

In the National Bank of India dual-database service, Pankaj designed the system to write the account change and an outbox event in the same single savings transaction, then relay the event asynchronously to the loan database. This gave eventual consistency and resilience without the cost of distributed transactions.

Remember one sentence: one @Transactional equals one DataSource, and cross-database atomicity requires JTA or an outbox pattern.

---

Share
Use ← → arrow keys to navigate