Without a pool, every call to DriverManager.getConnection() creates a brand new connection:

StepTypical Cost
TCP connect1–5 ms (local) / 50–200 ms (remote)
TLS handshake10–50 ms
DB authentication5–20 ms
Total per query (no pool)60–270 ms overhead
Total per query (with pool)<1 ms (borrow from pool)

A pool also caps the number of concurrent connections to the database, preventing connection exhaustion.

<dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.1.0</version> </dependency> <!-- Plus your JDBC driver --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.7.3</version> </dependency>

Create a HikariConfig, configure it, then build a HikariDataSource. The DataSource is the pool — keep it as a singleton in your application:

import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; public class DataSourceFactory { private static final HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); // Connection settings config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb"); config.setUsername("myuser"); config.setPassword("secret"); config.setDriverClassName("org.postgresql.Driver"); // optional — auto-detected // Pool sizing config.setMaximumPoolSize(10); // max connections in pool config.setMinimumIdle(2); // keep at least 2 idle connections // Timeout settings config.setConnectionTimeout(30_000); // ms to wait for a connection config.setIdleTimeout(600_000); // ms idle before connection is evicted config.setMaxLifetime(1_800_000); // ms max connection lifetime // Pool name (appears in logs) config.setPoolName("MyAppPool"); dataSource = new HikariDataSource(config); } public static HikariDataSource get() { return dataSource; } }

Use it exactly like any other DataSource:

try (Connection conn = DataSourceFactory.get().getConnection(); PreparedStatement ps = conn.prepareStatement("SELECT count(*) FROM users"); ResultSet rs = ps.executeQuery()) { rs.next(); System.out.println("Users: " + rs.getLong(1)); }
PropertyDefaultPurpose
maximumPoolSize10Hard ceiling on total connections
minimumIdle= maxPoolConnections kept warm when idle
connectionTimeout30 000 msMax wait for a free connection; throws SQLTimeoutException
idleTimeout600 000 msTime an idle connection can sit in the pool before eviction
maxLifetime1 800 000 msAbsolute max connection age — prevents stale TCP connections
keepaliveTime0 (disabled)Period to ping idle connections to keep them alive through firewalls
connectionTestQuerynoneSQL to validate connections — use only if driver lacks JDBC 4 isValid()
leakDetectionThreshold0 (disabled)Log a warning if a connection is held longer than this (ms)
schemadriver defaultDefault schema for new connections
autoCommittrueInitial auto-commit for borrowed connections

Bigger is not better. Every extra connection uses server memory and adds contention. The HikariCP author recommends:

# Pool size formula (PostgreSQL team research) pool_size = (core_count * 2) + effective_spindle_count # For a 4-core server with SSDs (spindles = 1): pool_size = (4 * 2) + 1 = 9 → round up to 10 For most web applications, 10–20 connections is more than enough. A pool of 100 connections usually performs worse than a pool of 10 due to lock contention inside the database.

If code borrows a connection but never returns it (missing try-with-resources), the pool eventually starves. Enable leak detection to log a warning with a stack trace pointing to the offending call site:

config.setLeakDetectionThreshold(5_000); // warn if connection held > 5 s

Sample log output:

WARN HikariPool-1 - Connection leak detection triggered for com.example.OrderService.createOrder(OrderService.java:47), stack trace follows... Set leakDetectionThreshold to a value longer than any legitimate long-running query you have. Setting it too low creates false positives for batch jobs.

Instead of code, you can configure HikariCP from a properties file on the classpath:

# src/main/resources/hikari.properties dataSourceClassName=org.postgresql.ds.PGSimpleDataSource dataSource.serverName=localhost dataSource.portNumber=5432 dataSource.databaseName=mydb dataSource.user=myuser dataSource.password=secret maximumPoolSize=10 minimumIdle=2 connectionTimeout=30000 idleTimeout=600000 maxLifetime=1800000 poolName=MyAppPool HikariConfig config = new HikariConfig("/hikari.properties"); HikariDataSource ds = new HikariDataSource(config);

Close the pool when the application exits to release all connections cleanly:

Runtime.getRuntime().addShutdownHook(new Thread(() -> { if (!DataSourceFactory.get().isClosed()) { DataSourceFactory.get().close(); System.out.println("Connection pool closed"); } }));