Connection Pooling

Efficiently managing database connections

← Back to Index

Why Connection Pooling?

Creating a database connection is an expensive operation. It involves network handshakes, authentication, memory allocation, and resource initialization. Without pooling, each database request would create a new connection, severely impacting performance.

The Problem: Connection Creation Cost

// Without pooling - SLOW!
public User findUser(long id) {
    // Step 1: Create connection (100-500ms!)
    // - TCP handshake with database server
    // - SSL/TLS negotiation (if enabled)
    // - Authentication (username/password)
    // - Session setup on database
    Connection conn = DriverManager.getConnection(url, user, password);  // SLOW!

    // Step 2: Execute query (1-10ms)
    User user = executeQuery(conn, id);  // Fast

    // Step 3: Close connection (5-20ms)
    conn.close();  // Resources freed, connection destroyed

    return user;
}

// For 1000 requests per second:
// Without pooling: 1000 * 200ms = 200 seconds of connection overhead!
// With pooling:    1000 * 1ms = 1 second (reuse existing connections)

The Solution: Connection Pool

// With pooling - FAST!
┌─────────────────────────────────────────────────────────────────────┐
│                      CONNECTION POOL                                 │
│                                                                      │
│   ┌─────┐  ┌─────┐  ┌─────┐  ┌─────┐  ┌─────┐                      │
│   │Conn1│  │Conn2│  │Conn3│  │Conn4│  │Conn5│  ... (pre-created)   │
│   │ OK  │  │BUSY │  │ OK  │  │ OK  │  │BUSY │                      │
│   └─────┘  └─────┘  └─────┘  └─────┘  └─────┘                      │
│                                                                      │
│   getConnection() → Returns available connection instantly!          │
│   close() → Returns connection to pool (doesn't destroy it)         │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

public User findUser(long id) {
    // Step 1: Get connection from pool (<1ms!)
    Connection conn = dataSource.getConnection();  // Already created!

    // Step 2: Execute query (1-10ms)
    User user = executeQuery(conn, id);

    // Step 3: Return to pool (<1ms)
    conn.close();  // Doesn't close! Returns to pool for reuse.

    return user;
}

Connection Pooling Benefits:

  • Performance: Eliminate connection creation overhead
  • Resource Control: Limit total connections to database
  • Stability: Handle connection failures gracefully
  • Monitoring: Track connection usage and health

Popular Connection Pools

Pool Performance Features Best For
HikariCP Fastest Lightweight, simple, bytecode optimization Most applications (Spring Boot default)
Apache DBCP2 Good Mature, commons-pool based Legacy applications
c3p0 Good Automatic retry, statement caching Hibernate (legacy)
Tomcat JDBC Pool Very Good Async, interceptors Tomcat applications
Vibur DBCP Very Good Simple, well-documented Alternative to HikariCP
Recommendation

Use HikariCP for new projects. It's the fastest, most reliable, and is the default in Spring Boot. The creator is exceptionally dedicated to performance optimization.

HikariCP Configuration

Maven Dependency

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>5.1.0</version>
</dependency>

<!-- Spring Boot includes HikariCP by default with: -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Programmatic Configuration

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

public class DatabaseConfig {

    public static HikariDataSource createDataSource() {
        HikariConfig config = new HikariConfig();

        // Essential settings
        config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
        config.setUsername("user");
        config.setPassword("password");
        config.setDriverClassName("com.mysql.cj.jdbc.Driver");

        // Pool sizing (most important!)
        config.setMaximumPoolSize(10);        // Max connections in pool
        config.setMinimumIdle(5);             // Min idle connections to maintain

        // Timeouts
        config.setConnectionTimeout(30000);   // 30s - wait for connection from pool
        config.setIdleTimeout(600000);        // 10min - idle connection timeout
        config.setMaxLifetime(1800000);       // 30min - max connection lifetime
        config.setKeepaliveTime(300000);      // 5min - keepalive query interval

        // Validation
        config.setConnectionTestQuery("SELECT 1");  // Optional for JDBC4+ drivers

        // Performance
        config.setAutoCommit(true);            // Default auto-commit state
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");

        // Pool name (for monitoring)
        config.setPoolName("MyAppPool");

        return new HikariDataSource(config);
    }
}

Spring Boot Configuration

# application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: user
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver

    # HikariCP specific settings
    hikari:
      pool-name: MyAppPool
      maximum-pool-size: 10
      minimum-idle: 5
      idle-timeout: 600000
      max-lifetime: 1800000
      connection-timeout: 30000
      keepalive-time: 300000

      # MySQL optimizations
      data-source-properties:
        cachePrepStmts: true
        prepStmtCacheSize: 250
        prepStmtCacheSqlLimit: 2048
        useServerPrepStmts: true
        useLocalSessionState: true
        rewriteBatchedStatements: true
        cacheResultSetMetadata: true
        cacheServerConfiguration: true
        elideSetAutoCommits: true
        maintainTimeStats: false

Configuration Explained

// Pool Size Configuration - The most critical settings!

┌─────────────────────────────────────────────────────────────────────┐
│                    POOL SIZING VISUALIZATION                         │
│                                                                      │
│   maximumPoolSize: 10                                               │
│   ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│   │  1  │ │  2  │ │  3  │ │  4  │ │  5  │ │  6  │ │  7  │ │  8  │ │
│   └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│   │ minimumIdle: 5 (always ready) │  │ Created on demand  │        │
│   └───────────────────────────────┘  └────────────────────┘        │
│                                                                      │
│   - Start with minimumIdle connections                              │
│   - Create more as needed, up to maximumPoolSize                    │
│   - Destroy excess idle connections after idleTimeout               │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

// Key parameters:

// maximumPoolSize: Total connections the pool can have
// - Too low: Requests queue waiting for connections
// - Too high: Database overwhelmed, memory wasted
// - Formula: connections = ((core_count * 2) + effective_spindle_count)
// - For SSD: core_count * 2 + 1 (typically 5-10 for most apps)

// minimumIdle: Connections maintained even when idle
// - Set equal to maximumPoolSize for consistent performance
// - Set lower to reduce resource usage during idle periods

// connectionTimeout: How long to wait for a connection
// - If pool exhausted and no connection available within this time
// - SQLException is thrown
// - 30 seconds is a good default

// idleTimeout: How long a connection can sit idle before removal
// - Only applies when connections > minimumIdle
// - Set to 10 minutes for most cases

// maxLifetime: Maximum connection lifetime
// - Should be less than database's wait_timeout
// - MySQL default wait_timeout: 8 hours
// - Set to 30 minutes for safety

Pool Sizing Guidelines

The Formula

// HikariCP Creator's Formula:
// connections = ((core_count * 2) + effective_spindle_count)

// For a 4-core server with SSD:
// connections = (4 * 2) + 1 = 9

// Why so few?
// - More connections ≠ More throughput
// - Database spends time context-switching between connections
// - Each connection uses memory (both app and DB side)

// Analogy: Highway lanes
// - 4-lane highway at capacity: cars moving smoothly
// - Add 100 more lanes: traffic jams at intersections!

Common Scenarios

// Scenario 1: Simple Web Application
// - 4 core server, 100 concurrent users
maximumPoolSize: 10
minimumIdle: 5

// Scenario 2: High-Traffic API
// - 8 core server, microservice, fast queries
maximumPoolSize: 15
minimumIdle: 10

// Scenario 3: Batch Processing
// - Long-running queries, fewer concurrent
maximumPoolSize: 5
minimumIdle: 2
connectionTimeout: 60000  // Longer timeout

// Scenario 4: Multiple Datasources
// - Main DB + Read replica
// - Split pool size between them
mainPool.maximumPoolSize: 8
readReplicaPool.maximumPoolSize: 5

Calculating Based on Load

// If you know your workload:

// Given:
// - 1000 requests/second
// - Average query time: 10ms
// - Maximum query time: 100ms

// Minimum connections needed:
// connections = requests/second × average_query_time
// connections = 1000 × 0.01 = 10

// Add buffer for spikes:
// maximumPoolSize = 15

// Monitor and adjust based on:
// - Connection wait times
// - Pool utilization
// - Database CPU/connections
Common Mistake

Don't set pool size to match concurrent users! 10,000 users don't need 10,000 connections. Most users are idle (reading pages, filling forms). A pool of 10-20 connections can serve thousands of users.

Monitoring and Troubleshooting

HikariCP Metrics

import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.HikariPoolMXBean;

public class PoolMonitor {

    private final HikariDataSource dataSource;

    public void printStats() {
        HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();

        System.out.println("=== HikariCP Stats ===");
        System.out.println("Active Connections: " + poolMXBean.getActiveConnections());
        System.out.println("Idle Connections: " + poolMXBean.getIdleConnections());
        System.out.println("Total Connections: " + poolMXBean.getTotalConnections());
        System.out.println("Threads Waiting: " + poolMXBean.getThreadsAwaitingConnection());
    }
}

// Spring Boot Actuator integration
// Add to application.yml:
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,hikaricp

// Access metrics at:
// GET /actuator/metrics/hikaricp.connections.active
// GET /actuator/metrics/hikaricp.connections.idle
// GET /actuator/metrics/hikaricp.connections.pending

Common Issues and Solutions

// Issue 1: Connection Timeout (pool exhausted)
// Error: "Connection is not available, request timed out after 30000ms"

// Causes:
// - Pool too small for load
// - Connection leaks (not closed properly)
// - Slow queries holding connections

// Solutions:
// 1. Enable leak detection:
config.setLeakDetectionThreshold(60000);  // Warn if connection held > 60s

// 2. Find and fix leaks:
try (Connection conn = dataSource.getConnection()) {
    // Always use try-with-resources!
}

// 3. Review slow queries
config.addDataSourceProperty("logger", "com.mysql.cj.log.Slf4JLogger");
config.addDataSourceProperty("profileSQL", "true");

// Issue 2: Connection Reset / Stale Connections
// Error: "Communications link failure" or "Connection reset"

// Cause: Database closed idle connections

// Solution: Configure keepalive and max lifetime
config.setMaxLifetime(1800000);    // 30 minutes
config.setKeepaliveTime(300000);   // 5 minutes

// Issue 3: Too Many Connections
// Error: "Too many connections" from database

// Cause: Multiple app instances, each with large pool

// Solution: Calculate total across all instances
// 5 app instances × 10 connections = 50 database connections
// MySQL default max_connections = 151

Logging Configuration

# logback.xml - Enable HikariCP debug logging
<logger name="com.zaxxer.hikari" level="DEBUG"/>
<logger name="com.zaxxer.hikari.HikariConfig" level="DEBUG"/>

# application.yml
logging:
  level:
    com.zaxxer.hikari: DEBUG
    com.zaxxer.hikari.HikariConfig: DEBUG

# Sample debug output:
# HikariPool-1 - Pool stats (total=10, active=2, idle=8, waiting=0)

Connection Pool with JPA/Hibernate

<!-- persistence.xml with HikariCP -->
<persistence-unit name="myPU">
    <properties>
        <!-- HikariCP as connection provider -->
        <property name="hibernate.connection.provider_class"
                  value="org.hibernate.hikaricp.internal.HikariCPConnectionProvider"/>

        <!-- HikariCP settings -->
        <property name="hibernate.hikari.dataSourceClassName"
                  value="com.mysql.cj.jdbc.MysqlDataSource"/>
        <property name="hibernate.hikari.dataSource.url"
                  value="jdbc:mysql://localhost:3306/mydb"/>
        <property name="hibernate.hikari.dataSource.user"
                  value="user"/>
        <property name="hibernate.hikari.dataSource.password"
                  value="password"/>
        <property name="hibernate.hikari.maximumPoolSize"
                  value="10"/>
        <property name="hibernate.hikari.minimumIdle"
                  value="5"/>
    </properties>
</persistence-unit>

// Spring Boot - automatic (no extra config needed!)
// Just add spring-boot-starter-data-jpa and configure datasource
// HikariCP is used automatically

Other Connection Pools

Apache DBCP2

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-dbcp2</artifactId>
    <version>2.11.0</version>
</dependency>

// Configuration
BasicDataSource ds = new BasicDataSource();
ds.setUrl("jdbc:mysql://localhost:3306/mydb");
ds.setUsername("user");
ds.setPassword("password");
ds.setMaxTotal(10);
ds.setMaxIdle(5);
ds.setMinIdle(2);
ds.setMaxWaitMillis(30000);

Tomcat JDBC Pool

<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-jdbc</artifactId>
    <version>10.1.18</version>
</dependency>

// Configuration
PoolProperties p = new PoolProperties();
p.setUrl("jdbc:mysql://localhost:3306/mydb");
p.setUsername("user");
p.setPassword("password");
p.setMaxActive(10);
p.setMaxIdle(5);
p.setMinIdle(2);
p.setMaxWait(30000);

DataSource ds = new DataSource(p);

Best Practices

Do's

  • Always use try-with-resources for connections
  • Set maxLifetime lower than database timeout
  • Enable leak detection in development
  • Monitor pool metrics in production
  • Start small with pool size and increase if needed
  • Use same pool settings across all app instances

Don'ts

  • Don't set huge pool sizes (100+ connections)
  • Don't ignore connection leaks
  • Don't hold connections while doing non-DB work
  • Don't use DriverManager directly (bypasses pool)
  • Don't set different pool configs for different environments without testing

Summary

  • Purpose: Reuse connections instead of creating new ones
  • HikariCP: Fastest and most reliable (Spring Boot default)
  • Pool Size: (cores * 2) + 1 for SSD, typically 5-15
  • Key Settings: maximumPoolSize, minimumIdle, connectionTimeout
  • Leak Detection: Enable in dev to find unclosed connections
  • Monitoring: Watch active connections and wait times
  • Try-with-resources: Always use to prevent leaks