Backend Development 15 min read

Why HikariCP Is So Fast: An In‑Depth Look at Its Design and Implementation

This article explains the core concepts of connection‑pool technology, details the architectural choices behind HikariCP such as dual HikariPool instances, FastList, custom ConcurrentBag, ThreadLocal caching, and byte‑code generation, and walks through the key source code that makes the pool exceptionally fast.

Architect's Guide
Architect's Guide
Architect's Guide
Why HikariCP Is So Fast: An In‑Depth Look at Its Design and Implementation

Connection‑Pool Technology

A connection pool is a buffer that creates and manages reusable connections; it consists of three parts: pool creation, connection usage management, and pool shutdown. The core idea is connection reuse, which improves efficiency and safety for database and other resources.

HikariCP Overview

HikariCP, an open‑source JDBC pool from a Japanese developer, is described in its README as "Fast, simple, reliable" and "zero‑overhead". The library is lightweight (≈130 KB) and became the default pool in Spring Boot 2.0, gaining popularity for its speed.

Why HikariCP Is So Fast

Two HikariPool objects : one final fastPathPool avoids lazy initialization, the other volatile pool adds minimal overhead.

FastList instead of ArrayList : FastList removes range‑check logic in get and scans from the tail in remove , matching the typical open‑then‑close order of connections.

Custom concurrent collection : a hand‑rolled ConcurrentBag provides faster concurrent access than standard collections.

Thread‑local fast path : the same thread retrieves a connection from a ThreadLocal cache, eliminating contention.

Byte‑code minimisation : Javassist generates lightweight proxy classes, producing less byte‑code than JDK dynamic proxies.

HikariCP Core Classes

HikariConfig holds database and pool configuration. HikariDataSource implements DataSource and provides two constructors: a no‑arg constructor (leaving fastPathPool null) and a constructor that accepts a HikariConfig and creates both pool instances.

private final HikariPool fastPathPool;
private volatile HikariPool pool;

public HikariDataSource() {
    super();
    fastPathPool = null;
}

public HikariDataSource(HikariConfig configuration) {
    configuration.validate();
    configuration.copyStateTo(this);
    LOGGER.info("{} - Starting...", configuration.getPoolName());
    pool = fastPathPool = new HikariPool(this);
    LOGGER.info("{} - Start completed.", configuration.getPoolName());
    this.seal();
}

The dual‑pool design allows the final fastPathPool to be used directly for the common case, while the volatile pool provides a fallback during lazy initialization.

HikariPool Construction

The constructor creates a ConcurrentBag for connections, a SuspendResumeLock , a blocking queue for connection‑adder tasks, and a scheduled HouseKeeper that periodically calls fillPool() to maintain the minimum number of idle connections.

public HikariPool(final HikariConfig config) {
    super(config);
    this.connectionBag = new ConcurrentBag<>(this);
    this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;
    LinkedBlockingQueue
addConnectionQueue = new LinkedBlockingQueue<>(maxPoolSize);
    this.addConnectionExecutor = createThreadPoolExecutor(addConnectionQueue, poolName + " connection adder", threadFactory, new ThreadPoolExecutor.DiscardOldestPolicy());
    this.closeConnectionExecutor = createThreadPoolExecutor(maxPoolSize, poolName + " connection closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
    this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, housekeepingPeriodMs, MILLISECONDS);
    ...
}

Creating a Connection

fillPool() calculates how many connections to add and submits creation tasks to addConnectionExecutor . The PoolEntryCreator implements Callable and repeatedly creates PoolEntry objects while the pool is normal.

public Boolean call() {
    while (poolState == POOL_NORMAL && shouldCreateAnotherConnection()) {
        final PoolEntry poolEntry = createPoolEntry();
        if (poolEntry != null) {
            connectionBag.add(poolEntry);
            return Boolean.TRUE;
        }
    }
    return Boolean.FALSE;
}

createPoolEntry() builds a PoolEntry that wraps a newly opened JDBC Connection . private PoolEntry createPoolEntry() { final PoolEntry poolEntry = newPoolEntry(); ... } PoolEntry newPoolEntry() throws Exception { return new PoolEntry(newConnection(), this, isReadOnly, isAutoCommit); }

Getting a Connection

The public getConnection() first checks fastPathPool , then falls back to pool . If both are null, double‑checked locking creates the pool lazily.

public Connection getConnection() throws SQLException {
    if (isClosed()) {
        throw new SQLException("HikariDataSource " + this + " has been closed.");
    }
    if (fastPathPool != null) {
        return fastPathPool.getConnection();
    }
    HikariPool result = pool;
    if (result == null) {
        synchronized (this) {
            result = pool;
            if (result == null) {
                validate();
                LOGGER.info("{} - Starting...", getPoolName());
                try {
                    pool = result = new HikariPool(this);
                    this.seal();
                } catch (PoolInitializationException pie) {
                    if (pie.getCause() instanceof SQLException) {
                        throw (SQLException) pie.getCause();
                    } else {
                        throw pie;
                    }
                }
                LOGGER.info("{} - Start completed.", getPoolName());
            }
        }
    }
    return result.getConnection();
}

Inside HikariPool#getConnection() the thread first tries to borrow a connection from its ThreadLocal list, then from the shared ConcurrentBag , and finally blocks on the hand‑off queue if none are immediately available.

Releasing a Connection

When Connection.close() is called, HikariCP closes any open statements (stored in a FastList ) and then recycles the PoolEntry . The recycle operation marks the entry as not‑in‑use, offers it to the waiting queue, and optionally caches it in the thread‑local list for fast future retrieval.

public void requite(final T bagEntry) {
    bagEntry.setState(STATE_NOT_IN_USE);
    for (int i = 0; waiters.get() > 0 && i < 1000; i++) {
        if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
            return;
        }
        if ((i & 0xff) == 0xff) {
            parkNanos(MICROSECONDS.toNanos(10));
        } else {
            Thread.yield();
        }
    }
    List
threadLocalList = threadList.get();
    if (threadLocalList.size() < 50) {
        threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
    }
}

Conclusion

The source‑code walk reveals many tiny optimisations—final fields, volatile usage, custom fast collections, thread‑local caching, and byte‑code generation—that together give HikariCP its reputation for speed. Paying attention to such details can yield substantial performance gains in everyday backend development.

Further Reading

For more deep‑dive articles on backend performance and architecture, follow the linked resources at the end of the original post.

Javaperformance optimizationbackend developmentConnection PoolJDBCHikariCP
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.