Back to Connection Pooling
Q2Question 2 of 6

How would you build a database connection pool from scratch, and how does HikariCP improve on it?

Key Concepts
  • The simplest correct pool is a bounded `BlockingQueue<Connection>` pre-filled with open connections at startup.
  • Borrowing calls `queue.poll(timeout, unit)` which blocks when empty and fails fast after the timeout expires.
  • Releasing calls `queue.offer(connection)` which returns the connection to the queue and unblocks the next waiting borrower.
  • The naive pool has weaknesses: lock contention under heavy concurrency, no validation of broken connections, no lifetime recycling, and no leak detection.
  • HikariCP replaces the locking queue with a lock-free `ConcurrentBag` using thread-local storage so a thread can reclaim its recently used connection without contention.
  • HikariCP adds connection validation before handout, `maxLifetime` recycling to survive database restarts, and leak detection that logs the offending stack trace.
  • A pool guarantees two essentials: a hard cap on connections (the queue never grows) and backpressure (borrowers wait rather than flooding the database).

Definition

The naive pool is a `BlockingQueue` you `take` from and `offer` back; HikariCP replaces the locking queue with a lock-free `ConcurrentBag` and adds validation, lifetime recycling, and leak detection.
  • From-scratch pool – A bounded BlockingQueue pre-filled with a fixed number of open connections. Borrowing calls take() (blocks when empty), releasing calls offer() (returns the connection).
  • Two guarantees – A hard cap on connections (the queue never grows beyond its initial size) and backpressure (borrowers wait rather than flooding the database).
  • HikariCP improvements – A lock-free ConcurrentBag with thread-local affinity, connection validation before handout, maxLifetime recycling, and leak detection with stack trace logging.
  • Why it matters – Pool contention and oversizing are the real performance killers, and HikariCP's lock-free design specifically targets contention.

Picture a tray at the National Bank of India holding exactly ten passbooks, one per open ledger line. A teller takes a passbook to serve Rajesh; if the tray is empty, they wait until someone puts one back. When done, they drop it back on the tray. That tray is the BlockingQueue. HikariCP is a smarter tray: it remembers which teller last used which passbook so that teller can grab it again instantly without elbowing anyone, it checks each passbook is still valid before handing it out, it replaces old worn passbooks, and it rings an alarm if a teller walks off and never returns one.

Understanding the Concepts

What is the simplest correct connection pool?

The simplest correct pool is a bounded BlockingQueue pre-filled with a fixed number of open connections. You decide a size, open that many physical connections at startup, and put them all in the queue.

JAVA
package com.bank.pool;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

public class SimpleConnectionPool {

    private final BlockingQueue<Connection> pool;
    private final int size;

    public SimpleConnectionPool(String url, String user, String password, int size)
            throws SQLException {
        this.size = size;
        this.pool = new ArrayBlockingQueue<>(size);
        for (int i = 0; i < size; i++) {
            pool.offer(DriverManager.getConnection(url, user, password));
        }
    }

    public Connection borrow(long timeoutMs) throws InterruptedException {
        Connection conn = pool.poll(timeoutMs, TimeUnit.MILLISECONDS);
        if (conn == null) {
            throw new RuntimeException(
                    "Connection not available, timed out after " + timeoutMs + " ms");
        }
        return conn;
    }

    public void release(Connection conn) {
        if (conn != null) {
            pool.offer(conn);  // Returns connection to the queue
        }
    }

    public int getAvailableCount() {
        return pool.size();
    }
}

This gives two essential behaviors: a hard cap on connections (the queue never holds more than size) and backpressure (borrowers wait via poll with timeout rather than overwhelming the database).

How do you use the from-scratch pool?

Rajesh uses the simple pool to serve balance requests at the National Bank of India.

JAVA
package com.bank.service;

import com.bank.pool.SimpleConnectionPool;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class BalanceService {

    private final SimpleConnectionPool pool;

    public BalanceService(SimpleConnectionPool pool) {
        this.pool = pool;
    }

    public BigDecimal getBalance(String accountId) throws Exception {
        Connection conn = pool.borrow(3000);  // Wait max 3 seconds
        try {
            String sql = "SELECT balance FROM accounts WHERE account_id = ?";
            try (PreparedStatement ps = conn.prepareStatement(sql)) {
                ps.setString(1, accountId);
                try (ResultSet rs = ps.executeQuery()) {
                    return rs.next() ? rs.getBigDecimal("balance") : BigDecimal.ZERO;
                }
            }
        } finally {
            pool.release(conn);  // Always return, even on exception
        }
    }
}
BASH
javac -d out com/bank/pool/SimpleConnectionPool.java com/bank/service/BalanceService.java
java -cp out com.bank.service.BalanceService

What are the weaknesses of the naive pool?

The naive BlockingQueue pool works but has four real weaknesses that HikariCP fixes.

JAVA
package com.bank.pool;

import java.sql.Connection;
import java.sql.SQLException;

public class NaivePoolWeaknesses {

    // Weakness 1: Lock contention
    // LinkedBlockingQueue uses internal locks
    // Under 100 concurrent threads, many contend on the same lock
    // Throughput drops as thread count increases

    // Weakness 2: No validation
    // If the database dropped a connection (timeout, restart),
    // the pool hands out a dead connection
    public void demonstrateDeadConnection(Connection conn) throws SQLException {
        // This query fails because the connection was dropped
        conn.prepareStatement("SELECT 1").executeQuery();
        // PSQLException: This connection has been closed
    }

    // Weakness 3: No lifetime recycling
    // A connection pinned for days accumulates stale server-side state
    // After a database failover, old connections point to the wrong server

    // Weakness 4: No leak detection
    // If a developer forgets pool.release(conn), the connection is lost
    // The pool silently shrinks until it is exhausted
    // No warning, no stack trace to find the offending code
}

How does HikariCP's ConcurrentBag improve on the BlockingQueue?

HikariCP replaces the BlockingQueue with a ConcurrentBag, a lock-free structure with thread-local storage. When a thread releases a connection, that connection is kept near that thread. When the same thread needs a connection again, it grabs its own recently-used connection without touching shared state.

JAVA
package com.bank.config;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;

public class HikariPoolConfig {

    public DataSource createPool() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:postgresql://localhost:5432/nbi_ledger");
        config.setUsername("nbi_app");
        config.setPassword("secret");
        config.setMaximumPoolSize(10);

        // Connection validation: lightweight isValid() call
        // Dead connections are discarded and replaced before reaching code
        config.setConnectionTestQuery("SELECT 1"); // Fallback if isValid not supported

        // Lifetime recycling: retire after 30 minutes
        config.setMaxLifetime(1800000);

        // Leak detection: log stack trace if held > 5 seconds
        config.setLeakDetectionThreshold(5000);

        return new HikariDataSource(config);
    }
}

In a request-per-thread server, the thread that served the last request and released connection C3 will get C3 back on the next request, with nearly zero contention. This is the common case and makes borrow and release almost lock-free.

How does HikariCP validate connections?

HikariCP validates connections before handing them out. If a connection has been dropped by the database (timeout, restart, network issue), HikariCP discards it and creates a fresh one so your code never sees a dead connection.

JAVA
package com.bank.pool;

// HikariCP validation flow (conceptual)
public class ValidationFlow {

    // Step 1: Thread calls dataSource.getConnection()
    // Step 2: ConcurrentBag picks a connection (thread-local first)
    // Step 3: HikariCP calls connection.isValid(timeout)
    //         or runs the connectionTestQuery
    // Step 4a: If valid → hand it to the caller
    // Step 4b: If invalid → discard, create new, validate, hand out

    // With the naive BlockingQueue pool:
    // Step 1: Thread calls pool.borrow()
    // Step 2: Queue returns a connection
    // Step 3: NO VALIDATION
    // Step 4: If the connection is dead, the query fails
    //         The developer sees: PSQLException: This connection has been closed
}

How does HikariCP detect connection leaks?

If a borrowed connection is not returned within leakDetectionThreshold milliseconds, HikariCP logs a warning with the full stack trace pointing at the code that forgot to close it.

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.sql.DataSource;

public class LeakyLoanService {

    private final DataSource dataSource;

    public LeakyLoanService(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    // BUG: connection is never closed on the exception path
    public BigDecimal getLoanBalance(String loanId) throws SQLException {
        Connection conn = dataSource.getConnection();
        PreparedStatement ps = conn.prepareStatement(
                "SELECT outstanding FROM loans WHERE loan_id = ?");
        ps.setString(1, loanId);
        ResultSet rs = ps.executeQuery();
        // If an exception occurs here, conn is never returned!
        BigDecimal balance = rs.next() ? rs.getBigDecimal(1) : BigDecimal.ZERO;
        conn.close();
        return balance;
    }
    // HikariCP logs after 5 seconds:
    // WARN  Apparent connection leak detected (held for 5001ms)
    // java.lang.Exception: Apparent connection leak
    //   at com.bank.service.LeakyLoanService.getLoanBalance(LeakyLoanService.java:15)
}

The stack trace points directly at line 15 where the connection was borrowed but not properly returned.

How does maxLifetime recycling work?

HikariCP retires and recreates connections after maxLifetime milliseconds. This ensures that connections do not linger forever with stale server-side state, and that the pool survives database restarts and failovers gracefully.

DIAGRAM
Connection lifecycle with maxLifetime = 30 minutes:

Time 0:00  → C1 created, added to pool
Time 0:01  → C1 borrowed, used for balance query, returned
Time 0:05  → C1 borrowed again, used for transfer, returned
...
Time 29:59 → C1 is still in pool
Time 30:00 → HikariCP retires C1, creates C1' as replacement
             If database restarted at 15:00, C1 would have been stale
             maxLifetime prevents that

What should a senior engineer know?

A senior engineer should understand that the naive pool's value is pedagogical: it teaches the two guarantees (hard cap and backpressure). In production, HikariCP's lock-free design, validation, recycling, and leak detection are essential. HikariCP deliberately keeps the pool small and fast rather than feature-heavy, because pool contention and oversizing are the real performance killers.

TEXT
Naive pool vs HikariCP comparison:

                     Naive BlockingQueue    HikariCP
Borrow mechanism     Lock-based take()      Lock-free ConcurrentBag
Release mechanism    Lock-based offer()     Thread-local return
Validation           None                   isValid() + test query
Lifetime recycling   None                   maxLifetime eviction
Leak detection       None                   Stack trace logging
Dead connection      Handed out, fails      Discarded, replaced
Thread affinity      None                   Thread-local preference

Visual Overview

DIAGRAM
┌────────────────────────────────────────────────────────────┐
│          Naive Pool vs HikariCP Architecture              │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  Naive Pool (BlockingQueue):                              │
│  ┌──────────────────────────────────────────┐             │
│  │ ArrayBlockingQueue<Connection>            │             │
│  │ ┌────┐ ┌────┐ ┌────┐ ... ┌────┐         │             │
│  │ │ C1 │ │ C2 │ │ C3 │     │ C10│         │             │
│  │ └────┘ └────┘ └────┘     └────┘         │             │
│  │ take() ──── LOCK ──── blocks if empty    │             │
│  │ offer() ─── LOCK ──── returns connection │             │
│  └──────────────────────────────────────────┘             │
│                                                            │
│  HikariCP (ConcurrentBag):                                │
│  ┌──────────────────────────────────────────┐             │
│  │ ConcurrentBag<PoolEntry>                  │             │
│  │                                           │             │
│  │ Thread A → [C1] (thread-local, no lock)  │             │
│  │ Thread B → [C2] (thread-local, no lock)  │             │
│  │ Shared  → [C3][C4]...[C10]               │             │
│  │                                           │             │
│  │ + Validation: isValid() before handout    │             │
│  │ + maxLifetime: retire after 30 min        │             │
│  │ + Leak detection: stack trace after 5s    │             │
│  └──────────────────────────────────────────┘             │
└────────────────────────────────────────────────────────────┘

Key Takeaway

The simplest correct connection pool is a bounded BlockingQueue. You open a fixed number of connections at startup and put them in the queue. Borrowing calls poll(timeout, unit) which atomically removes a connection and blocks when the queue is empty until the timeout expires. Releasing calls offer() which puts the connection back and wakes the next waiting borrower. This gives two things a pool must guarantee: a hard cap on connections because the queue never grows beyond its initial size, and backpressure because borrowers wait rather than flooding the database.

That naive pool works but has real weaknesses. A standard blocking queue uses locks, so under heavy concurrency many threads contend on the same lock. It never validates connections, so it can hand out a dead one after the database drops it. It never retires old connections, so stale connections linger and failovers are rough. And it cannot tell you when a borrower forgot to return a connection, which is the classic cause of pool exhaustion.

HikariCP improves on every one of these. Its core is a lock-free ConcurrentBag with thread-local storage, so a thread that releases a connection can reclaim that same connection almost without contention on the next request. It validates connections before handing them out using isValid(). It enforces maxLifetime that retires and recreates connections so the pool survives database restarts. And it provides leak detection that logs a stack trace when a connection is held longer than the configured threshold.

At the National Bank of India, Rajesh built a BlockingQueue pool to understand the guarantees, but in production the team used HikariCP because a leaked connection during a load test had exhausted the hand-rolled pool silently. HikariCP's leak detection would have pointed straight at the offending code with a stack trace.

Remember one sentence: the naive pool is a blocking queue you take from and offer back, and HikariCP replaces the locking queue with a lock-free concurrent bag and adds validation, lifetime recycling, and leak detection.

---

Share
Use ← → arrow keys to navigate