Skip to main content

Design a Database Connection Pool (HikariCP style)

Problem Statement

Design a database connection pool manager (similar to HikariCP). The manager should initialize a set of database connections, allow multiple application threads to borrow connections, handle timeout configurations when connections are unavailable, and reclaim connections when threads finish. Crucially, the pool must prevent clients from closing the physical connections directly, routing closure actions back to the pool instead.

Asked In Companies

Functional Requirements

  • Maintain a pool of active database connections up to a configurable maximum size.
  • Allow threads to lease connections: getConnection(timeout).
  • Block threads temporarily if all connections are in use, throwing a timeout exception if the limit is breached.
  • Reclaim connections when the client calls close(), returning them to the available pool rather than destroying the socket.
  • Ensure thread safety during connection allocation, return, and lookup operations.

Objects Required

  • Connection (Base interface mapping standard DB interactions)
  • PhysicalConnection (Concrete mock representing raw network connections)
  • ProxyConnection (Decorator proxy that overrides closing logic)
  • ConnectionPool (Core pool manager and factory)

Connection Interface & PhysicalConnection

The Connection interface matches standard JDBC behaviors. Decorating this interface prevents our pool logic from tightly coupling to physical network classes.


public interface Connection {
    void execute(String sql);
    void close();
}

The execute() method simulates running queries, while close() handles lifecycle cleanups. Here is the concrete mock implementation:


public class PhysicalConnection implements Connection {
    private final int id;

    public PhysicalConnection(int id) {
        this.id = id;
    }

    @Override
    public void execute(String sql) {
        System.out.println("Executing SQL on physical connection #" + id + ": " + sql);
    }

    @Override
    public void close() {
        System.out.println("Destroying physical socket for connection #" + id);
    }
}

The PhysicalConnection represents the real TCP link to the database. We mock its execution outputs and track its ID for debugging purposes.


ProxyConnection Class

To ensure connections are returned to the pool instead of destroyed when the client calls close(), we wrap the real connection inside a proxy wrapper. This is a classic application of the **Proxy Design Pattern**.


public class ProxyConnection implements Connection {
    private final Connection realConnection;
    private final ConnectionPool pool;

    public ProxyConnection(Connection realConnection, ConnectionPool pool) {
        this.realConnection = realConnection;
        this.pool = pool;
    }

    @Override
    public void execute(String sql) {
        realConnection.execute(sql);
    }

    @Override
    public void close() {
        System.out.println("Close intercepted. Returning connection to the pool.");
        pool.releaseConnection(this);
    }

    public Connection getRealConnection() {
        return realConnection;
    }
}

The overridden close() method delegates resource cleanup back to the ConnectionPool object. Any client calling this method returns the wrapper to the active stack, keeping the physical socket alive.


ConnectionPool Class

The ConnectionPool coordinates active states, tracks borrowed entities, and exposes thread-safe borrowing mechanics. We utilize a concurrent BlockingQueue to block and wake threads dynamically.


import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

public class ConnectionPool {
    private final int maxPoolSize;
    private final LinkedBlockingQueue<Connection> availableConnections;
    private final Set<Connection> activeConnections;
    private int connectionIdCounter = 0;

    public ConnectionPool(int initialSize, int maxPoolSize) {
        this.maxPoolSize = maxPoolSize;
        this.availableConnections = new LinkedBlockingQueue<>(maxPoolSize);
        this.activeConnections = Collections.newSetFromMap(new ConcurrentHashMap<>());

        for (int i = 0; i < initialSize; i++) {
            availableConnections.add(createNewConnection());
        }
    }

    private synchronized Connection createNewConnection() {
        connectionIdCounter++;
        return new PhysicalConnection(connectionIdCounter);
    }

    public Connection getConnection(long timeoutMs) {
        try {
            // Retrieve available connection, waiting if none exist up to timeout limits
            Connection conn = availableConnections.poll(timeoutMs, TimeUnit.MILLISECONDS);
            if (conn == null) {
                // Check if we can dynamically grow the pool to maxPoolSize
                synchronized (this) {
                    if (availableConnections.size() + activeConnections.size() < maxPoolSize) {
                        conn = createNewConnection();
                    }
                }
                if (conn == null) {
                    throw new RuntimeException("Timeout error: No available connections in pool.");
                }
            }

            ProxyConnection proxy = new ProxyConnection(conn, this);
            activeConnections.add(proxy);
            return proxy;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Thread interrupted while borrowing connection.");
        }
    }

    public void releaseConnection(Connection proxyConnection) {
        if (proxyConnection instanceof ProxyConnection) {
            ProxyConnection proxy = (ProxyConnection) proxyConnection;
            if (activeConnections.remove(proxy)) {
                availableConnections.offer(proxy.getRealConnection());
            }
        }
    }
}

Here is an explanation of the core operations inside the ConnectionPool class:

  • The constructor configures size limits and prepopulates the queue with mock connections.
  • createNewConnection() acts as our internal factory, instantiating new physical connection records with incremental IDs.
  • getConnection() attempts to poll from the available queue. If empty, it attempts to scale to maximum pool capacity. If it still fails, it throws a timeout error. On success, it creates a new ProxyConnection wrapper.
  • releaseConnection() removes the wrapper instance from our active tracker set and returns the unwrapped realConnection to the available queue.

Main Driver Class

This class tests our database connection pool. It initializes a small pool, borrows connections, handles threshold limits, triggers timeout blocks, and demonstrates connection recycle recovery paths.


public class Main {
    public static void main(String[] args) {
        // Create pool: initial size = 2, max size = 3
        ConnectionPool pool = new ConnectionPool(2, 3);

        System.out.println("--- Borrowing Connection 1 ---");
        Connection conn1 = pool.getConnection(1000);
        conn1.execute("SELECT * FROM users");

        System.out.println("\n--- Borrowing Connection 2 ---");
        Connection conn2 = pool.getConnection(1000);
        conn2.execute("SELECT * FROM orders");

        System.out.println("\n--- Borrowing Connection 3 (Scales pool to max capacity) ---");
        Connection conn3 = pool.getConnection(1000);
        conn3.execute("SELECT * FROM products");

        // Attempting to borrow when pool is completely exhausted
        try {
            System.out.println("\n--- Borrowing Connection 4 (Expect timeout exception) ---");
            Connection conn4 = pool.getConnection(1000);
            conn4.execute("SELECT * FROM logs");
        } catch (Exception e) {
            System.out.println("Caught Expected Exception: " + e.getMessage());
        }

        // Return connection 1 to release pool pressure
        System.out.println("\n--- Returning Connection 1 ---");
        conn1.close();

        // Try to borrow again
        System.out.println("\n--- Borrowing Connection 4 again (Should succeed now) ---");
        Connection conn4 = pool.getConnection(1000);
        conn4.execute("SELECT * FROM logs");
        conn4.close();
    }
}

The main() driver initializes the resources, allocates connections up to maximum limits, demonstrates safety assertions, and displays how physical connections are recycled rather than terminated.


Class Diagram

Connectionexecute(sql: String): voidclose(): voidPhysicalConnectionid: intexecute(sql: String): voidclose(): voidProxyConnectionrealConnection: Connectionpool: ConnectionPoolexecute(sql: String): voidclose(): voidgetRealConnection(): ConnectionConnectionPoolmaxPoolSize: intavailableConnections: BlockingQueue<Connection>activeConnections: Set<Connection>connectionIdCounter: intgetConnection(timeoutMs: long): ConnectionreleaseConnection(connection: Connection): voidcreateNewConnection(): ConnectionMainmain(args: String[]): voidwraps11returns to11instantiates & configuresmanages1manydrives

Also See

Comments

Popular posts from this blog

Designing a Parking Lot - Low Level Design

Problem Statement Design a parking lot that can handle vehicles entering and leaving while managing parking across multiple floors. Each vehicle should be assigned a suitable parking spot based on its type, and the spot should be freed once the vehicle exits. The design should also support generating a ticket at entry and optionally calculating the parking fee based on the duration of stay. Asked In Companies Amazon Google Microsoft Uber Walmart Flipkart Meta PayPal Oracle Salesforce Adobe Apple Intuit LinkedIn Atlassian Functional Requirements The design should support multiple vehicle types such as bikes, cars, and trucks A vehicle must be assigned a parking spot compatible with its type A parking spot cannot be assigned to more than one vehicle at a time The parking lot should support multiple levels (floors) The design should search and allocate an availa...

Most Frequently Asked Low Level Design(LLD) Interview Questions

Below are the curated list of most commonly asked Low Level Design (LLD) interview problems. Each problem includes a short description and a link to the complete solution with code and class diagrams. Design Parking Lot System The system should handle parking for different vehicle types such as bikes, cars, and trucks. It should manage slot allocation, availability tracking, and entry/exit flow. The design also ensures efficient usage of parking space under varying load conditions. View Solution Design Elevator / Lift System The system should support multiple elevators operating across floors with request handling logic. It focuses on scheduling algorithms to minimize wait time and optimize movement. It also manages direction control and concurrent floor requests. View Solution Design Movie Ticket Booking System The system should allow users to browse movies, select shows, and book seats. It handles seat ...

Software Design Patterns for LLD Interviews: A Complete Guide

Software Design Patterns for LLD Interviews: A Complete Guide In Software Development Engineer (SDE) interviews—especially for mid-level and senior roles—low-level design (LLD) rounds assess your ability to write clean, reusable, maintainable, and extensible code. The foundation of resolving these architectural challenges lies in the standard Gang of Four (GoF) Design Patterns. Rather than memorizing theoretical definitions, interviewers expect you to apply these patterns to real-world scenarios, identifying the trade-offs of each. Below is a comprehensive guide to the 12 most frequently asked design patterns in LLD interviews, categorized by their classification (Creational, Structural, and Behavioral). Each pattern contains a concrete, real-world Java implementation and a detailed breakdown of design decisions. Creational Design Patterns Creational design patterns deal with object creation mechanisms. They abstract the instantiation process, making a system independent of how...