To use connection pooling in plain JDBC with HikariCP, the main shift is:
- stop using
DriverManager.getConnection(...)everywhere - create one
DataSource(the pool) at startup - whenever you need a DB connection, call
dataSource.getConnection() - always close resources with try-with-resources (closing returns the connection to the pool, it does not kill the physical connection)
1) Create a pooled DataSource once
A simple “factory” that builds a singleton pool:
package org.kodejava.jdbc;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import java.time.Duration;
public final class DataSourceFactory {
private static final HikariDataSource dataSource = create();
private DataSourceFactory() {}
private static HikariDataSource create() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/app_db");
config.setUsername("db_user");
config.setPassword("db_password"); // use env vars/secret store in real apps
// Pool sizing (tune per app + DB limits)
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
// Timeouts
config.setConnectionTimeout(Duration.ofSeconds(5).toMillis()); // wait for a connection from pool
config.setIdleTimeout(Duration.ofMinutes(5).toMillis());
config.setMaxLifetime(Duration.ofMinutes(30).toMillis());
// Optional: validation / observability
config.setPoolName("AppHikariPool");
return new HikariDataSource(config);
}
public static DataSource getDataSource() {
return dataSource;
}
/** Call this on application shutdown */
public static void shutdown() {
dataSource.close();
}
}
Notes:
maximumPoolSizeis usually the most important setting.- Prefer one pool per database, not one per DAO/class.
2) Use it in JDBC code (and always close)
Example query using the pooled DataSource:
package org.kodejava.jdbc;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class UserRepository {
private final DataSource dataSource;
public UserRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
public String findEmailById(long id) throws SQLException {
String sql = "select email from users where id = ?";
try (Connection con = dataSource.getConnection();
PreparedStatement ps = con.prepareStatement(sql)) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getString("email") : null;
}
}
}
}
Key point: con.close() (done by try-with-resources) returns the connection to the pool.
3) Shutdown cleanly
If you’re writing a CLI app / desktop app / simple server, ensure the pool is closed on exit:
package org.kodejava.jdbc;
public class App {
public static void main(String[] args) throws Exception {
var ds = DataSourceFactory.getDataSource();
var repo = new UserRepository(ds);
System.out.println(repo.findEmailById(1L));
DataSourceFactory.shutdown();
}
}
For long-running apps, register a shutdown hook:
Runtime.getRuntime().addShutdownHook(new Thread(DataSourceFactory::shutdown));
4) Common configuration tips (practical)
- Pool size: start with
maximumPoolSize=10for typical web apps, then tune using metrics and DB limits. - Don’t set
minimumIdletoo high unless you truly need warm connections. - Transactions: still work the same (use
con.setAutoCommit(false)andcommit/rollback), but make sure you always return the connection to the pool. - If you see “connection leak” warnings, it usually means some path didn’t close the connection (missing try-with-resources).
Maven dependencies
<dependencies>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>6.3.0</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.7</version>
</dependency>
</dependencies>
