How to Configure Two DataSources in Spring Boot

Introduction

An application keeps its own data in one database and has to read from, or write to, a second one - a reporting database, a legacy system, an audit store. Spring Boot configures one DataSource from spring.datasource.*, and adding a second DataSource bean the obvious way makes the first one disappear or causes "expected single matching bean" errors. This quick tip shows the setup Spring Boot 4 recommends, which keeps the auto-configured DataSource and adds a second one next to it.

Tested on Spring Boot 4.1.2, Spring Framework 7.0.10, HikariCP 7.0.2, H2 2.4.240 and JDK 25.

Adding a second DataSource next to the auto-configured one

Declare the second DataSource with @Bean(defaultCandidate = false) and a @Qualifier: it is then injected only where it is asked for by name, and Spring Boot keeps configuring the first one as before. Its settings are bound from a prefix of your own with @ConfigurationProperties -
The short answer
@Bean(defaultCandidate = false)
@Qualifier("audit")
@ConfigurationProperties("app.audit.datasource")
HikariDataSource auditDataSource() {
  return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
application.properties
spring.datasource.url=jdbc:h2:mem:shop
app.audit.datasource.jdbc-url=jdbc:h2:mem:audit

Two DataSources in a running application

The example has a shop database, configured by Spring Boot, with a book table from schema.sql, and an audit database added as above. Each database gets its own JdbcClient.
src/main/resources/application.properties
spring.main.banner-mode=off
logging.level.root=WARN
# the first DataSource - auto-configured by Spring Boot
spring.datasource.url=jdbc:h2:mem:shop
# the second DataSource - bound by @ConfigurationProperties("app.audit.datasource")
app.audit.datasource.jdbc-url=jdbc:h2:mem:audit
app.audit.datasource.maximum-pool-size=2
src/main/resources/schema.sql
create table book (isbn varchar(20) primary key, title varchar(100));
insert into book values ('978-1', 'Effective Java');
DataSourceConfig.java
//DataSourceConfig.java
package com.javabrahman.tips.datasources;

import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
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.jdbc.core.simple.JdbcClient;

@Configuration
public class DataSourceConfig {

  @Bean(defaultCandidate = false)
  @Qualifier("audit")
  @ConfigurationProperties("app.audit.datasource")
  HikariDataSource auditDataSource() {
    return DataSourceBuilder.create().type(HikariDataSource.class).build();
  }

  @Bean(defaultCandidate = false)
  @Qualifier("audit")
  JdbcClient auditJdbcClient(@Qualifier("audit") DataSource auditDataSource) {
    return JdbcClient.create(auditDataSource);
  }
}
TwoDataSourcesApp.java
//TwoDataSourcesApp.java
package com.javabrahman.tips.datasources;

import java.util.List;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.simple.JdbcClient;

@SpringBootApplication
public class TwoDataSourcesApp {

  public static void main(String[] args) {
    SpringApplication.run(TwoDataSourcesApp.class, args);
  }

  @Bean
  CommandLineRunner demo(JdbcClient shop, @Qualifier("audit") JdbcClient audit) {
    return args -> {
      audit.sql("create table audit_log (id int auto_increment primary key, message varchar(200))").update();
      String title = shop.sql("select title from book where isbn = '978-1'").query(String.class).single();
      audit.sql("insert into audit_log (message) values (?)").param("read book: " + title).update();

      System.out.println("shop  JdbcClient -> database " + database(shop) + ", tables " + tables(shop));
      System.out.println("audit JdbcClient -> database " + database(audit) + ", tables " + tables(audit));
      System.out.println("audit log: " + audit.sql("select message from audit_log").query(String.class).list());
    };
  }

  private static String database(JdbcClient jdbc) {
    return jdbc.sql("select database()").query(String.class).single();
  }

  private static List<String> tables(JdbcClient jdbc) {
    return jdbc.sql("select table_name from information_schema.tables where table_schema = 'PUBLIC'").query(String.class).list();
  }
}
 OUTPUT of the application
shop  JdbcClient -> database SHOP, tables [BOOK]
audit JdbcClient -> database AUDIT, tables [AUDIT_LOG]
audit log: [read book: Effective Java]
Explanation of the output
  • Each JdbcClient reaches its own database: select database() answers SHOP for the auto-configured one and AUDIT for the one with the audit qualifier.
  • The injection point JdbcClient shop has no qualifier and still received the shop database, because the audit beans are not default candidates. Spring Boot's auto-configured DataSource and JdbcClient stayed in place.
  • The shop database has the BOOK table from schema.sql, the audit database only the AUDIT_LOG table the runner created. schema.sql was applied to the auto-configured DataSource only.
The usual first attempt writes the second database's address as url, the name used under spring.datasource. Run with that file, the application fails on the first use of the audit database -
src/main/resources/wrong.properties - the usual first attempt
spring.main.banner-mode=off
logging.level.root=WARN
spring.datasource.url=jdbc:h2:mem:shop
app.audit.datasource.url=jdbc:h2:mem:audit
 OUTPUT with app.audit.datasource.url (the lines that name the error)
2026-11-13T23:01:01.170+05:30 ERROR 56316 --- [           main] com.zaxxer.hikari.HikariConfig           : HikariPool-2 - dataSource or dataSourceClassName or jdbcUrl is required.
java.lang.IllegalArgumentException: dataSource or dataSourceClassName or jdbcUrl is required.

Pitfalls

  • Under your own prefix the properties are those of the pool class, not Spring Boot's: HikariCP's address property is jdbc-url, so url is ignored and the pool fails with "jdbcUrl is required", as above. Other Hikari settings, such as maximum-pool-size, are set under the same prefix.
  • Leaving out defaultCandidate = false makes the second bean an ordinary DataSource. Spring Boot's own DataSource is only created when no such bean exists, so the auto-configured one is no longer created.
  • schema.sql, data.sql, Flyway and Liquibase run against the auto-configured DataSource only. The second database needs its own initialization.
  • @Transactional uses the transaction manager of the auto-configured DataSource. Work on the second database that must be transactional needs a DataSourceTransactionManager for it, named in @Transactional("auditTransactionManager").
  • With Spring Data JPA, each database also needs its own EntityManagerFactory and an @EnableJpaRepositories for the repositories that use it; the Spring Boot reference shows this in Using Multiple EntityManagerFactoriesClick to read: Using Multiple EntityManagerFactories.

Related Spring Boot tutorials

How Spring Boot configures a DataSource and JdbcClient is in JdbcClient and Spring Data JDBCClick to read tutorial: JdbcClient and Spring Data JDBC: When Not to Use JPA; binding settings with @ConfigurationProperties is in type-safe configuration with @ConfigurationPropertiesClick to read tutorial: Type-Safe Configuration with @ConfigurationProperties, and choosing between beans of the same type in @Qualifier, @Primary and conditional beansClick to read tutorial: Spring @Qualifier, @Primary and Conditional Beans.