Spring Boot
Connection Pooling
Connection pooling maintains a reusable set of pre-established database connections, eliminating the overhead of creating and tearing down a TCP connection and authenticating with the database on every query. Without a pool, each database operation takes 20-100ms just to establish the connection — with a pool, a connection is available in microseconds. Spring Boot auto-configures HikariCP as the default pool.
Why Connection Pooling Matters
Opening a database connection involves a TCP handshake, TLS negotiation (for SSL connections), database authentication, and session initialisation. This takes 20–100ms on a local network and 100–500ms over a WAN. An application making 1000 requests per second without pooling spends more time opening connections than executing queries.
A connection pool solves this by keeping a fixed number of connections open and recycling them. Each request borrows a connection from the pool, uses it, and returns it. The cost of opening the connection is paid once at startup, not on every request.
The pool also acts as a concurrency limiter — if all connections are in use, new requests wait rather than overwhelming the database with more connections than it can handle. Most databases have a hard maximum on concurrent connections (PostgreSQL default: 100, MySQL default: 151) and degrade sharply when that limit is approached.
Shell
# ── Without pooling (each request opens a new connection): ───────────
# Request → open TCP → TLS handshake → auth → query → close TCP
# Cost: 20-100ms overhead per request
# 1000 req/s = 1000 connection open/close cycles per second
# ── With pooling (connections reused): ────────────────────────────────
# Startup: open N connections → put in pool
# Request → borrow connection → query → return connection
# Cost: microseconds to borrow from pool
# 1000 req/s = same N connections, reused continuously
# ── Connection lifecycle in a pool: ───────────────────────────────────
# 1. Pool created at startup — minIdle connections opened immediately
# 2. Request arrives — pool hands out an idle connection
# 3. Request completes — connection returned to pool (not closed)
# 4. Idle connection ages past idleTimeout — pool closes and removes it
# 5. Remaining connections < minIdle — pool opens new connections
# 6. Connection ages past maxLifetime — pool closes and replaces it
# 7. All connections in use and request arrives — request waits up to
# connectionTimeout, then throws SQLTimeoutException
# ── What happens without proper pool configuration: ───────────────────
# Pool too small: requests queue up → high latency under load
# Pool too large: database overwhelmed → OOM, context switching overhead
# maxLifetime too long: stale connections after DB restart or firewall timeout
# No keepalive: DB closes idle connections → "Communications link failure"Connection Pool Architecture
A connection pool manages three categories of connections: active (currently in use by a thread), idle (available for borrowing), and maintenance (being tested, evicted, or replaced). Understanding these states explains the configuration parameters.
Shell
# ── Pool states: ─────────────────────────────────────────────────────
#
# ┌─────────────────────────────────────────────────────────────┐
# │ Connection Pool │
# │ │
# │ ACTIVE connections IDLE connections PENDING borrows │
# │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
# │ │ conn1 (busy) │ │ conn4 (free) │ │ thread A │ │
# │ │ conn2 (busy) │ │ conn5 (free) │ │ thread B │ │
# │ │ conn3 (busy) │ └──────────────┘ └─────────────┘ │
# │ └──────────────┘ waiting for a │
# │ free connection │
# └─────────────────────────────────────────────────────────────┘
#
# maximumPoolSize = max(active + idle)
# minimumIdle = target number of idle connections to maintain
# connectionTimeout = max time a thread waits for a connection
#
# ── What happens when pool is exhausted: ─────────────────────────────
# All N connections are active. New request arrives.
# → Request queues (waits up to connectionTimeout)
# → A connection is returned before timeout → request proceeds
# → Timeout expires before a connection is returned
# → com.zaxxer.hikari.pool.HikariPool$PoolInitializationException
# or java.sql.SQLTimeoutException
# ── Pool sizing rule of thumb: ────────────────────────────────────────
# PostgreSQL paper (from Percona):
# optimal pool size = ((core_count * 2) + effective_spindle_count)
# For most cloud VMs (4-8 cores, SSD): pool size of 10-20 is typical.
# Above 20-30 connections: performance usually degrades (lock contention).
# For read replicas or write-heavy workloads: tune separately.Spring Boot Pool Auto-Configuration
Spring Boot selects and configures a connection pool automatically based on what is on the classpath. HikariCP is preferred and is included transitively by spring-boot-starter-data-jpa. The selection order is HikariCP → Tomcat JDBC → DBCP2 → Oracle UCP.
yaml
# ── Pool selection priority: ─────────────────────────────────────────
# 1. HikariCP — default, included with spring-boot-starter-data-jpa
# 2. Tomcat JDBC — if HikariCP not present
# 3. DBCP2 — if neither above
# 4. Oracle UCP — if Oracle JDBC + UCP on classpath
# ── Force a specific pool (override auto-selection): ─────────────────
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource # HikariCP
# type: org.apache.tomcat.jdbc.pool.DataSource # Tomcat JDBC
# type: org.apache.commons.dbcp2.BasicDataSource # DBCP2
# ── Verify which pool is active (check startup logs): ─────────────────
logging:
level:
com.zaxxer.hikari: DEBUG # HikariCP configuration logs
org.springframework.boot.autoconfigure.jdbc: DEBUG
# ── Startup log confirms pool type: ──────────────────────────────────
# HikariPool-1 - Starting...
# HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@1a2b3c4d
# HikariPool-1 - Start completed.
# ── Excluding HikariCP to use Tomcat JDBC: ────────────────────────────
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<exclusions>
<exclusion>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>Multiple DataSources
Applications that connect to more than one database require multiple DataSource beans, each with its own connection pool. One DataSource must be marked @Primary so Spring Boot's auto-configuration knows which to use by default.
yaml
# ── application.yml — two datasources: ───────────────────────────────
spring:
datasource:
primary:
url: jdbc:postgresql://primary-db:5432/mydb
username: appuser
password: ${PRIMARY_DB_PASSWORD}
hikari:
pool-name: primary-pool
maximum-pool-size: 10
reporting:
url: jdbc:postgresql://reporting-db:5432/reports
username: reporter
password: ${REPORTING_DB_PASSWORD}
hikari:
pool-name: reporting-pool
maximum-pool-size: 5
read-only: true # hint to pool — connections opened read-only
// ── Configuration class — declare both pools: ─────────────────────────
@Configuration
public class DataSourceConfig {
// Primary datasource — @Primary used by all default auto-configuration:
@Bean
@Primary
@ConfigurationProperties("spring.datasource.primary")
public DataSourceProperties primaryDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
@ConfigurationProperties("spring.datasource.primary.hikari")
public HikariDataSource primaryDataSource(
@Qualifier("primaryDataSourceProperties")
DataSourceProperties properties) {
return properties.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
// Reporting datasource:
@Bean
@ConfigurationProperties("spring.datasource.reporting")
public DataSourceProperties reportingDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties("spring.datasource.reporting.hikari")
public HikariDataSource reportingDataSource(
@Qualifier("reportingDataSourceProperties")
DataSourceProperties properties) {
return properties.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
}
// ── Using the secondary datasource: ──────────────────────────────────
@Repository
public class ReportingRepository {
private final JdbcTemplate reportingJdbc;
public ReportingRepository(
@Qualifier("reportingDataSource") DataSource reportingDataSource) {
this.reportingJdbc = new JdbcTemplate(reportingDataSource);
}
public List<Map<String, Object>> getMonthlyRevenue() {
return reportingJdbc.queryForList(
"SELECT month, SUM(total) FROM monthly_revenue GROUP BY month");
}
}Pool Monitoring with Actuator
Spring Boot Actuator exposes connection pool metrics through Micrometer. These metrics are essential for diagnosing pool exhaustion, slow query impact, and pool sizing decisions.
yaml
# ── application.yml — expose pool metrics: ───────────────────────────
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
tags:
application: myapp # tag all metrics with app name
# ── Key HikariCP metrics (available at /actuator/metrics/...): ────────
# hikaricp.connections — total connections in pool
# hikaricp.connections.active — currently in use
# hikaricp.connections.idle — available for use
# hikaricp.connections.pending — threads waiting for a connection
# hikaricp.connections.acquire — time to acquire a connection (histogram)
# hikaricp.connections.creation — time to create a new connection
# hikaricp.connections.usage — time connection held by a thread
# hikaricp.connections.timeout.total — total connection timeout events
# hikaricp.connections.max — maximumPoolSize setting
# hikaricp.connections.min — minimumIdle setting
# ── Prometheus scrape endpoint — connect to Grafana: ──────────────────
# GET /actuator/prometheus
# hikaricp_connections_active{pool="myapp-pool"}
# hikaricp_connections_pending{pool="myapp-pool"}
# hikaricp_connections_acquire_seconds_max{pool="myapp-pool"}
# ── Alert thresholds (Grafana/Prometheus rules): ──────────────────────
# WARN when: hikaricp_connections_pending > 0 for > 30 seconds
# ALERT when: hikaricp_connections_pending > 5 for > 60 seconds
# WARN when: hikaricp_connections_acquire_seconds_max > 1.0 (1 second)
# ALERT when: hikaricp_connections_timeout_total increases > 0
// ── Programmatic pool inspection: ────────────────────────────────────
@Component
@RequiredArgsConstructor
public class PoolMonitor {
private final DataSource dataSource;
public void logPoolStats() {
if (dataSource instanceof HikariDataSource hikari) {
HikariPoolMXBean pool = hikari.getHikariPoolMXBean();
log.info("Pool stats — active: {}, idle: {}, pending: {}, total: {}",
pool.getActiveConnections(),
pool.getIdleConnections(),
pool.getThreadsAwaitingConnection(),
pool.getTotalConnections());
}
}
}