Database Connection Pooling in Java with HikariCP

Connection pooling is a technique used to improve performance in applications with dynamic database driven content. Opening and closing database connections may not seem like a costly expense but it can add up rather quickly. Let's assume it takes 5ms to establish a connection and 5ms to execute your query (Completely made up numbers), 50% of the time is establishing the connection. Extend this to thousands or tens of thousands of requests and there is a lot of wasted network time. Connection pools are essentially a cache of open database connections. Once you open and use a database connection instead of closing it you add it back to the pool. When you go to fetch a new connection if there is one available in the pool it will use that connection instead of establishing another.

Why use a connection pool?

Constantly opening and closing connections can be expensive. Cache and reuse.

When activity spikes you can limit the number of connections to the database. This will force code to block until a connection is available. This is especially helpful in distributed environments.

Split out common operations into multiple pools. For instance you can have a pool designated for OLAP connections and a pool for OLTP connections each with different configurations.

HikariCP

HikariCP is a very fast lightweight Java connection pool. The API and overall codebase is relatively small (A good thing) and highly optimized. It also does not cut corners for performance like many other Java connection pool implementations. The Wiki is highly informative and dives really deep. If you are not as interested in the deep dives you should at least read and watch the video on connection pool sizing.

Creating Connection pools

Let's create two connections pools one for OLTP (named transactional) queries and one for OLAP (named processing). We want them split so we can have a queue of reporting queries back up but allow critical transactional queries to still get priority (This is up to the database of course but we can help a bit). We can also easily configure different timeouts or transaction iscolation levels. For now we just just change their names and pool sizes.

Configuring the Pools

HikariCP offers several options for configuring the pool. Since we are fans of roll your own and already created our own Typesafe Configuration we will reuse that. Notice we are using some of Typesafe's configuration inheritance.

ConnectionPool Factory

Since we don't need any additional state a static factory method passing our config, MetricRegistry, and HealthCheckRegistry is sufficient. Once again Dropwizard Metrics makes an appearance hooking into our connection pool now. This will provide us with some very useful pool stats in the future.

ConnectionPool Implementation

Now that we have two separate configs for our transactional and processing pools lets initialize them. Once again we are not using DI and instead are using the enum singleton pattern for lazy initialized singletons. Feel free to use DI in your own implementations. We now have two different pools with different configs that each lazily wire themselves up on demand.

public class ConnectionPools {
private static final Logger logger = LoggerFactory.getLogger(ConnectionPools.class);
/*
* Normally we would be using the app config but since this is an example
* we will be using a localized example config.
*/
private static final Config conf = new Configs.Builder()
.withResource("examples/hikaricp/pools.conf")
.build();
/*
* This pool is made for short quick transactions that the web application uses.
* Using enum singleton pattern for lazy singletons
*/
private enum Transactional {
INSTANCE(ConnectionPool.getDataSourceFromConfig(conf.getConfig("pools.transactional"), Metrics.registry(), HealthChecks.getHealthCheckRegistry()));
private final HikariDataSource dataSource;
private Transactional(HikariDataSource dataSource) {
this.dataSource = dataSource;
}
public HikariDataSource getDataSource() {
return dataSource;
}
}
public static HikariDataSource getTransactional() {
return Transactional.INSTANCE.getDataSource();
}
/*
* This pool is designed for longer running transactions / bulk inserts / background jobs
* Basically if you have any multithreading or long running background jobs
* you do not want to starve the main applications connection pool.
*
* EX.
* You have an endpoint that needs to insert 1000 db records
* This will queue up all the connections in the pool
*
* While this is happening a user tries to log into the site.
* If you use the same pool they may be blocked until the bulk insert is done
* By splitting pools you can give transactional queries a much higher chance to
* run while the other pool is backed up.
*/
private enum Processing {
INSTANCE(ConnectionPool.getDataSourceFromConfig(conf.getConfig("pools.processing"), Metrics.registry(), HealthChecks.getHealthCheckRegistry()));
private final HikariDataSource dataSource;
private Processing(HikariDataSource dataSource) {
this.dataSource = dataSource;
}
public HikariDataSource getDataSource() {
return dataSource;
}
}
public static HikariDataSource getProcessing() {
return Processing.INSTANCE.getDataSource();
}
public static void main(String[] args) {
logger.debug("starting");
DataSource processing = ConnectionPools.getProcessing();
logger.debug("processing started");
DataSource transactional = ConnectionPools.getTransactional();
logger.debug("transactional started");
logger.debug("done");
}
}