Many of you have probably already covered HikariCP pool size tuning and working backward from your Kubernetes replica count. But the moment your DataSource count grows to two or three—whether through a primary/replica setup or a multi-DB configuration—that formula alone is no longer enough.

This article won’t cover how to implement read/write splitting with AbstractRoutingDataSource. We’ll focus strictly on the calculation side: how to determine each maximumPoolSize when you have multiple DataSources.

The other trap: exceeding max_connections with multiple DataSources

As soon as you move to a primary/replica setup, your HikariPool count goes from one to two. With a multi-DB configuration, it increases even further. What’s tricky here is that “having more DataSources” actually encompasses two completely different patterns.

One pattern is where the primary and replica run on separate DB servers, each with its own independent max_connections. The other is where multiple databases live on the same DB server, sharing a single max_connections budget across the entire application. If you don’t distinguish between these two and just assume “we split the budget proportionally as DataSources increase,” your calculation will fall apart.

Reviewing the single-DataSource formula

For a single DataSource, the formula for working backward is simple:

replica count × maximumPoolSize + reserved connections ≤ max_connections

The derivation of this formula is covered in detail in the article on Kubernetes replica scaling, so check that out if you’re interested. What matters here is that this formula assumes a single target DB server.

With multiple DataSources, the first fork in the road is whether the budget is shared

For a primary/replica setup (separate DB servers, each with its own independent max_connections), things aren’t actually that complicated. You simply apply the single-DataSource formula independently to each DataSource.

Primary: Pod replica count × primaryPoolSize + reserved ≤ primary's max_connections
Replica: Pod replica count × replicaPoolSize + reserved ≤ replica's max_connections

For example, if the primary has max_connections=100 and the replica has max_connections=50, you’d calculate the primary pool as (100-10)/3 ≈ 30 and the replica pool as (50-5)/3 ≈ 15, each independently. There’s no need to combine the two into a single shared budget.

On the other hand, with a multi-DB configuration (multiple databases on the same DB server), max_connections is set at the server level, so every DataSource connecting to that server shares a single budget. This is the scenario where you actually need to do allocation math.

Making sure Σ(maximumPoolSize) × replica count doesn’t exceed the budget

The core formula for a multi-DB configuration looks like this:

Σ(maximumPoolSize of each DataSource sharing the same DB server) × Pod replica count + reserved connections ≤ max_connections

The only difference from the single-DataSource formula is that maximumPoolSize is no longer a single value—it’s the sum across all DataSources sharing the same server. Think of it as a two-step process: first secure the budget as a single variable, the “total pool size,” then allocate that total across individual DataSources.

Deciding how to allocate across DataSources

Once you’ve determined the total pool size, you need to decide how much of it goes to each DataSource.

  • Determine weights based on access frequency per use case (e.g., transactional DB vs. analytics DB)
  • Don’t guess at these weights—measure actual query ratios and wait times using slow query logs or APM data, then apply them
  • Avoid splitting evenly across the board. This tends to produce a lopsided situation where the busiest DataSource runs out first while others still have headroom to spare

Correctly estimating reserved connections

There’s one point that’s easy to misunderstand here. minimumIdle is a lower bound maintained within the range of maximumPoolSize—it is not something added on top of the budget. The official wiki also states that the default value equals maximumPoolSize, confirming it’s not a setting that reserves connections beyond the pool’s overall ceiling.

What you should account for as “reserved” is the administrative connections used by things like monitoring agents or migration tools (Flyway, Liquibase, etc.). For example, you might tally up specifics like: the monitoring agent holds 3 connections at all times, and migrations temporarily use 2 more. As a rule of thumb, reserving around 10% of the total DB budget is a safe bet.

Walking through a concrete calculation step by step

Let’s calculate for a multi-DB configuration where orders_db (transactional) and analytics_db (analytics) live on the same DB server, with 3 Pod replicas and max_connections=100.

Step 1: Determine the DB budget
  max_connections = 100
  Reserved connections (monitoring agent 3 + migration 2) = 5
  Actually available budget = 100 - 5 = 95

Step 2: Divide by Pod replica count to get the budget per instance
  95 / 3 ≈ 31 (total DataSource pool size per instance)

Step 3: Allocate across DataSources by weight
  Assume an APM-measured access ratio of orders:analytics = 7:3
  orders_db    = 31 × 0.7 ≈ 22
  analytics_db = 31 × 0.3 ≈ 9 (22+9=31, exactly matching the budget)

If applying the weights results in fractional values, round up the higher-priority DataSource (e.g., transactional) and round down the lower-priority one, so the total doesn’t exceed the budget.

Configuring pool sizes for multiple DataSources in application.yml

Reflect the calculation results as a separate configuration block for each DataSource.

app:
  datasource:
    orders:
      jdbc-url: jdbc:postgresql://shared-db:5432/orders_db
      hikari:
        pool-name: orders-pool
        # Step 3: 95 (budget) / 3 (replicas) × 0.7 (orders ratio) ≈ 22
        maximum-pool-size: 22
        minimum-idle: 22
    analytics:
      jdbc-url: jdbc:postgresql://shared-db:5432/analytics_db
      hikari:
        pool-name: analytics-pool
        # Step 3: 95 (budget) / 3 (replicas) × 0.3 (analytics ratio) ≈ 9
        maximum-pool-size: 9
        minimum-idle: 9

The reason for explicitly setting pool-name is that without it, you’d get an auto-generated name like HikariPool-1, making it impossible to identify which DataSource is having trouble just by looking at the logs. If you’re working with multiple DataSources, consider this setting essentially mandatory.

Symptoms and how to identify them when max_connections is exceeded

If the allocation is wrong, here’s how the symptoms show up in production:

org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection;
nested exception is java.sql.SQLTransientConnectionException: analytics-pool - Connection is not available, request timed out after 30000ms.

Notice that the error log includes the name you set via pool-name (analytics-pool in this example). Rather than every DataSource failing uniformly, it’s common for connection waits to occur on just one specific DataSource. By setting up monitoring with Micrometer/Actuator and comparing hikaricp.connections.pending across DataSources, you can quickly identify which budget is under pressure.

A pre-production sizing checklist

Before moving to implementation, confirm the following:

  • Have you correctly distinguished between separate servers (as in a primary/replica setup) and a shared server (as in a multi-DB configuration)?
  • For DataSources that share a budget, does the sum of maximumPoolSize × replica count fit within the budget after subtracting reserved connections?
  • Are you avoiding double-counting minimumIdle on top of the reserved amount (remember, minimumIdle sits inside maximumPoolSize)?
  • Has pool-name been set for each DataSource so they can be identified in the logs?
  • Is there an operational process in place to redo this calculation whenever you scale out your Pods?

Think of multiple DataSources in terms of “allocation,” not just “count”

The underlying approach—working backward from a fixed budget—doesn’t change from the single-DataSource case. What’s different is that you need to determine whether multiple DataSources share the same DB server’s budget, and only when they do is weighted allocation actually necessary.

For the fundamentals of single-DataSource tuning, see the HikariCP pool size tuning guide; for details on working backward from replica count, see the article on Kubernetes replica scaling; and for the implementation of read/write splitting itself, see the guide on read/write splitting with multiple DataSources.