How do I handle database timeouts in JDBC?

In JDBC, “database timeouts” can mean a few different things, and you handle each at a different layer. The most practical approach is to set timeouts deliberately and then catch the right exception types so you can decide whether to retry, fail fast, or surface a user-friendly error.

1) Connection timeout (can’t connect / handshake takes too long)

a) DriverManager login timeout (global)

This limits how long DriverManager will wait when establishing a connection.

package org.kodejava.jdbc;

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

public class ConnectTimeoutExample {
    public static void main(String[] args) throws SQLException {
        DriverManager.setLoginTimeout(10); // seconds

        try (Connection c = DriverManager.getConnection(
                "jdbc:mysql://localhost/kodejava",
                "kodejava",
                "s3cr*t"
        )) {
            // connected
        }
    }
}

b) Driver-specific connect/socket timeouts (recommended)

Most drivers expose properties like connectTimeout and socketTimeout (names vary by vendor). These are often more reliable than setLoginTimeout.

Example pattern using connection properties:

package org.kodejava.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

public class DriverPropertiesTimeoutExample {
    public static void main(String[] args) throws SQLException {
        Properties props = new Properties();
        props.setProperty("user", "kodejava");
        props.setProperty("password", "s3cr*t");

        // Vendor-specific keys; check your driver docs:
        props.setProperty("connectTimeout", "10000"); // ms (example)
        props.setProperty("socketTimeout", "30000");  // ms (example)

        try (Connection c = DriverManager.getConnection(
                "jdbc:mysql://localhost/kodejava",
                props
        )) {
            // ...
        }
    }
}

Rule of thumb: set both a connect timeout and a read/socket timeout, otherwise a query can hang at the network layer even if you set a query timeout.


2) Query execution timeout (a statement runs too long)

Use Statement.setQueryTimeout(int seconds) (works for Statement, PreparedStatement, CallableStatement). On timeout, drivers typically throw a SQLTimeoutException (a subclass of SQLException).

package org.kodejava.jdbc;

import java.sql.*;

public class QueryTimeoutExample {
    public static void main(String[] args) throws SQLException {
        try (Connection c = DriverManager.getConnection("jdbc:mysql://localhost/kodejava", "kodejava", "s3cr*t");
             PreparedStatement ps = c.prepareStatement("SELECT * FROM product WHERE price > ?")) {

            ps.setBigDecimal(1, new java.math.BigDecimal("100.00"));
            ps.setQueryTimeout(5); // seconds

            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    // consume results
                }
            }
        } catch (SQLTimeoutException e) {
            // This is your “query took too long” bucket.
            throw new RuntimeException("Query timed out; consider optimizing SQL or raising timeout.", e);
        }
    }
}

Important notes:

  • setQueryTimeout is enforced by the driver, and behavior can differ:
    • Some drivers send a cancel to the server.
    • Some only time out client-side.
  • If the thread is interrupted, or you want a manual escape hatch, you can also call Statement.cancel() from another thread.

3) Lock wait / deadlock timeouts (transaction waits too long)

These are not “JDBC timeouts” per se—they’re database concurrency timeouts. They usually surface as SQLException with:

  • SQLState like 40001 (serialization failure / deadlock, DB-dependent), or
  • vendor-specific error codes/messages (e.g., lock wait timeout exceeded).

Handling strategy:

  • Rollback the transaction.
  • Retry only if you can safely retry (best is retrying the whole transaction), and keep attempts small with backoff.

4) Pool acquisition timeout (you can’t get a Connection from the pool)

If you use a pool (HikariCP, DBCP, c3p0, etc.), also set a connection acquisition/checkout timeout. Otherwise, under load you’ll see “timeouts” that are actually “all connections are busy.”

This is configured on the pool, not via JDBC calls.


5) Catching and classifying timeouts correctly

Catch the specific subtype when possible

JDBC provides SQLTimeoutException:

try {
    // execute query/update
} catch (SQLTimeoutException e) {
    // query timeout bucket
} catch (SQLException e) {
    // everything else
}

Use SQLState for broad categories

If you need portability, SQLState prefixes help:

  • 08xxx → connection exception family (network/connection problems)
  • 40xxx → transaction rollback / concurrency issues (often retryable depending on DB)
static boolean isConnectionProblem(SQLException e) {
    String state = e.getSQLState();
    return state != null && state.startsWith("08");
}

6) Retry policy (only for the right failures)

Retries are useful for transient failures (deadlocks, lock timeouts, brief network blips), but dangerous for non-idempotent operations.

A safe baseline:

  • Retry 2–3 times max
  • Use jittered backoff
  • Retry only when:
    • you can retry the entire transaction, or
    • the operation is idempotent

Sketch:

package org.kodejava.jdbc;

import java.sql.SQLException;
import java.sql.SQLTimeoutException;
import java.time.Duration;
import java.util.concurrent.ThreadLocalRandom;

public class RetrySupport {
    public static <T> T withRetry(SqlSupplier<T> work) throws SQLException {
        int maxAttempts = 3;
        SQLException last = null;

        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                return work.get();
            } catch (SQLTimeoutException e) {
                // Query timed out: retry is usually NOT helpful unless you expect transient load.
                throw e;
            } catch (SQLException e) {
                last = e;
                if (!isRetryable(e) || attempt == maxAttempts) throw e;

                sleep(backoff(attempt));
            }
        }
        throw last; // unreachable
    }

    private static boolean isRetryable(SQLException e) {
        String state = e.getSQLState();
        if (state != null && state.startsWith("08")) return true;  // connection hiccup
        if (state != null && state.startsWith("40")) return true;  // tx rollback class (DB-dependent)
        return false;
    }

    private static Duration backoff(int attempt) {
        long baseMs = 100L * (1L << (attempt - 1)); // 100, 200, 400...
        long jitter = ThreadLocalRandom.current().nextLong(0, 100);
        return Duration.ofMillis(baseMs + jitter);
    }

    private static void sleep(Duration d) {
        try {
            Thread.sleep(d.toMillis());
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
        }
    }

    @FunctionalInterface
    public interface SqlSupplier<T> {
        T get() throws SQLException;
    }
}

7) Practical checklist (what to set in real apps)

  1. Pool acquisition timeout (if using a pool)
  2. Connect timeout (driver property)
  3. Socket/read timeout (driver property)
  4. Query timeout (setQueryTimeout)
  5. For transactions:
    • keep transactions short
    • handle deadlocks/lock timeouts with rollback and bounded retry

How do I handle SQL exceptions in JDBC for MySQL properly?

For MySQL (mysql-connector-j), “proper” SQLException handling is the same core approach as JDBC in general, plus a few MySQL-specific signals (SQLState + error code) that are worth using for translation/retry decisions.

1) Keep the important diagnostics (MySQL error code + SQLState)

MySQL gives you two invaluable fields:

  • e.getErrorCode()MySQL vendor error code (e.g., 1062 for duplicate key)
  • e.getSQLState()SQLState (often 23000, 40001, etc.)

A good pattern is: wrap once with context, but preserve those fields.

package org.kodejava.jdbc;

import java.sql.SQLException;

public final class MySqlExceptions {
    public static RuntimeException translate(String operation, SQLException e) {
        String msg = operation
                     + " failed (SQLState=" + e.getSQLState()
                     + ", errorCode=" + e.getErrorCode() + ")";

        // Keep e as the cause.
        return switch (e.getErrorCode()) {
            case 1062 ->
                    new IllegalStateException(msg + " - duplicate key", e); // unique constraint violation
            case 1213 ->
                    new IllegalStateException(msg + " - deadlock", e);      // often retryable
            case 1205 ->
                    new IllegalStateException(msg + " - lock wait timeout", e); // often retryable
            default -> new RuntimeException(msg, e);
        };
    }
}

2) MySQL error codes you’ll commonly care about

These are the ones that usually drive different handling:

Situation MySQL error code Typical SQLState What to do
Unique constraint violation (“Duplicate entry”) 1062 23000 Return “already exists” / map to 409 / domain error
Deadlock found 1213 40001 Often safe to retry the whole transaction
Lock wait timeout exceeded 1205 often 41000 Often retry or surface “please retry”
Foreign key constraint fails 1451/1452 23000 Map to domain validation (cannot delete/insert due to FK)
Connection/link failure varies 08xxx / 08S01 Treat as transient infra failure; maybe retry with backoff

Rule of thumb: prefer the vendor error code for MySQL-specific branching (it’s the most consistent), and keep SQLState for general categorization/logging.

3) Retrying safely (only for the right failures)

Only retry if:

  • the operation is idempotent, or you’re retrying the entire transaction from the beginning, and
  • the failure is one of the known transient classes (deadlock / lock timeout / connection hiccup).

A minimal “should retry?” helper:

package org.kodejava.jdbc;

import java.sql.SQLException;

public final class MySqlRetry {
    public static boolean isRetryable(SQLException e) {
        int code = e.getErrorCode();
        String state = e.getSQLState();

        // MySQL deadlock / lock wait timeout
        if (code == 1213 || code == 1205) return true;

        // Connection exception class (SQLState starts with "08")
        if (state != null && state.startsWith("08")) return true;

        return false;
    }
}

If you do retry, keep it small (e.g., 2–3 attempts) with jittered backoff, and log the final failure with the full chain (getNextException()) and suppressed exceptions.

4) Transactions: rollback without hiding the original error

With MySQL, rollback can also throw if the connection is broken. Best practice: attach rollback failure as suppressed so you don’t lose the root cause.

package org.kodejava.jdbc;

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

public final class TxUtil {
    public static void rollbackQuietly(Connection con, SQLException original) {
        try {
            con.rollback();
        } catch (SQLException rb) {
            original.addSuppressed(rb);
        }
    }
}

5) MySQL Connector/J note: you usually don’t need Class.forName(...)

With modern JDBC drivers (including MySQL Connector/J 8+), the driver is auto-registered via the Service Provider mechanism. Calling Class.forName("com.mysql.cj.jdbc.Driver") is typically unnecessary unless you’re in a very unusual classloading environment.

6) What to log (and what not to log)

Log:

  • operation name (e.g., "insert user")
  • SQLState, error code
  • exception chain (getNextException())
  • safe parameter identifiers (e.g., user id), not secrets

Avoid logging:

  • credentials
  • sensitive values (passwords, tokens)
  • huge SQL strings with embedded data (use prepared statements so you don’t have that problem)

How do I handle SQL exceptions in JDBC properly?

Handling SQLException “properly” in JDBC is mostly about (1) not leaking resources, (2) preserving diagnostic detail, (3) rolling back safely, and (4) translating errors into something meaningful at your app boundary.

1) Always close JDBC resources (use try-with-resources)

This eliminates most error-handling bugs (leaks and double-closes), and it also handles exceptions thrown during close() by attaching them as suppressed exceptions.

package org.kodejava.jdbc;

import javax.sql.DataSource;
import java.sql.*;

public final class JdbcExample {
    public static void runQuery(DataSource ds, long id) {
        String sql = "select name from users where id = ?";

        try (Connection con = ds.getConnection();
             PreparedStatement ps = con.prepareStatement(sql)) {

            ps.setLong(1, id);

            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    String name = rs.getString(1);
                    // use name...
                }
            }

        } catch (SQLException e) {
            throw toDataAccessException("Failed running query: " + sql, e);
        }
    }

    static RuntimeException toDataAccessException(String message, SQLException e) {
        // Keep the SQLException as the cause so details are not lost.
        return new RuntimeException(message + " (SQLState=" + e.getSQLState() + ", code=" + e.getErrorCode() + ")", e);
    }
}

Key points

  • Prefer PreparedStatement over Statement (safety + plan reuse).
  • Wrap/translate once, near your data-access boundary, but keep e as the cause.

2) Log/inspect the full JDBC error chain (and suppressed exceptions)

JDBC drivers can chain multiple exceptions (e.g., one per batch item), and try-with-resources can add suppressed exceptions from close().

package org.kodejava.jdbc;

import java.sql.SQLException;

public final class SqlDiagnostics {
    public static String describe(SQLException e) {
        StringBuilder sb = new StringBuilder();
        for (Throwable t = e; t != null; t = (t instanceof SQLException se) ? se.getNextException() : null) {
            if (t instanceof SQLException se) {
                sb.append("SQLException: message=").append(se.getMessage())
                        .append(", SQLState=").append(se.getSQLState())
                        .append(", code=").append(se.getErrorCode())
                        .append('\n');
            } else {
                sb.append("Throwable: ").append(t).append('\n');
            }

            for (Throwable sup : t.getSuppressed()) {
                sb.append("  suppressed: ").append(sup).append('\n');
            }
        }
        return sb.toString();
    }
}

When this matters

  • Batch updates (BatchUpdateException)
  • Failures during resource cleanup (network drop during close())

3) Handle transactions: commit/rollback with a safe rollback path

If you manually manage transactions (setAutoCommit(false)), your exception handling must:

  1. rollback on failure,
  2. not mask the original exception if rollback also fails,
  3. restore state if you’re reusing connections (pools usually reset, but don’t rely on it blindly).
package org.kodejava.jdbc;

import java.sql.*;

public final class JdbcTxExample {
    public static void transfer(Connection con, long fromId, long toId, long amount) throws SQLException {
        boolean oldAutoCommit = con.getAutoCommit();
        con.setAutoCommit(false);

        try (PreparedStatement debit = con.prepareStatement("update acct set bal = bal - ? where id = ?");
             PreparedStatement credit = con.prepareStatement("update acct set bal = bal + ? where id = ?")) {

            debit.setLong(1, amount);
            debit.setLong(2, fromId);
            debit.executeUpdate();

            credit.setLong(1, amount);
            credit.setLong(2, toId);
            credit.executeUpdate();

            con.commit();

        } catch (SQLException e) {
            try {
                con.rollback();
            } catch (SQLException rb) {
                e.addSuppressed(rb); // keep original, attach rollback failure for debugging
            }
            throw e; // or translate here
        } finally {
            try {
                con.setAutoCommit(oldAutoCommit);
            } catch (SQLException ac) {
                // typically log; don't hide earlier failure
            }
        }
    }
}

4) Don’t swallow exceptions; translate them at the right layer

Common strategy:

  • DAO/repository layer: catch SQLException, add context (operation + key parameters), then rethrow as:
    • a checked app exception (if you want callers to handle), or
    • a runtime “data access” exception (common in service-oriented apps).
  • Service/controller boundary: map to user-safe messages (avoid exposing SQL text / internals).

Avoid:

  • catch (SQLException e) {} (silences failures)
  • Throwing a new exception without e as cause (loses SQLState/vendor code)

5) Use SQLState / vendor codes for decisions (sparingly)

If you need conditional handling (e.g., unique constraint violation), prefer SQLState classes when possible:

  • 23*** integrity constraint violation (many DBs)
  • 40*** transaction rollback / serialization failure (often retryable)

But keep it minimal: drivers/databases vary.

6) Be careful with retries

Only retry when you can justify it:

  • transient network issues
  • deadlocks / serialization failures (often safe to retry the whole transaction)

Never retry blindly on all SQLExceptions.


Quick checklist

  • try-with-resources for Connection/Statement/ResultSet
  • Keep SQLException as the cause; don’t lose SQLState / error code
  • Walk getNextException() and check suppressed exceptions
  • In manual transactions: rollback in catch, attach rollback failures via addSuppressed
  • Translate exceptions at the repository boundary; expose safe messages at the edge

How do I configure C3P0 connection pool in JDBC?

To configure c3p0 as a JDBC connection pool, you typically:

  1. create a DataSource (usually ComboPooledDataSource)
  2. tune pool parameters (min/max pool size, idle time, timeouts, statement cache)
  3. use the DataSource everywhere instead of DriverManager.getConnection(...)
  4. close Connection/Statement/ResultSet normally (c3p0 returns connections to the pool on close())

Below are the common ways to do it.


1) Programmatic configuration (most common for plain JDBC)

package org.kodejava.jdbc;

import com.mchange.v2.c3p0.ComboPooledDataSource;

import javax.sql.DataSource;
import java.beans.PropertyVetoException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public final class C3p0DataSourceFactory {
    private static final ComboPooledDataSource ds = new ComboPooledDataSource();

    static {
        try {
            ds.setDriverClass("org.postgresql.Driver");
        } catch (PropertyVetoException e) {
            throw new ExceptionInInitializerError(e);
        }

        ds.setJdbcUrl("jdbc:postgresql://localhost:5432/<db>");
        ds.setUser("<db_user>");
        ds.setPassword("<db_password>");

        // Pool sizing
        ds.setMinPoolSize(5);
        ds.setInitialPoolSize(5);
        ds.setMaxPoolSize(20);
        ds.setAcquireIncrement(2);

        // Timeouts / lifecycles
        ds.setCheckoutTimeout(10_000);          // ms to wait for a connection
        ds.setMaxIdleTime(300);                 // seconds before idle conns are culled
        ds.setMaxConnectionAge(0);              // 0 = no limit (consider setting in prod)
        ds.setIdleConnectionTestPeriod(60);     // seconds between idle tests

        // Validation (pick ONE strategy; this is the most reliable)
        ds.setPreferredTestQuery("SELECT 1");
        ds.setTestConnectionOnCheckout(false);
        ds.setTestConnectionOnCheckin(true);

        // Statement caching (optional)
        ds.setMaxStatements(200);
        ds.setMaxStatementsPerConnection(20);
    }

    private C3p0DataSourceFactory() {}

    public static DataSource dataSource() {
        return ds;
    }

    // Optional: call on app shutdown (desktop apps / simple main())
    public static void shutdown() {
        ds.close();
    }

    // Example usage
    public static void main(String[] args) throws Exception {
        try (Connection c = dataSource().getConnection();
             PreparedStatement ps = c.prepareStatement("SELECT now()");
             ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                System.out.println(rs.getObject(1));
            }
        } finally {
            shutdown();
        }
    }
}

Notes on the key settings

  • minPoolSize / maxPoolSize: your main capacity knobs.
  • checkoutTimeout: prevents threads from waiting forever when the pool is exhausted.
  • Connection testing:
    • Prefer preferredTestQuery + testConnectionOnCheckin=true (or periodic tests) to avoid “broken connection” surprises.
    • testConnectionOnCheckout=true is safer but adds latency to every borrow.
  • Statement cache helps if you repeatedly run the same SQL.

2) Configure via c3p0.properties (cleaner config)

Create src/main/resources/c3p0.properties:

# Basic
c3p0.jdbcUrl=jdbc:postgresql://localhost:5432/<db>
c3p0.user=<db_user>
c3p0.password=<db_password>
c3p0.driverClass=org.postgresql.Driver

# Pool sizing
c3p0.minPoolSize=5
c3p0.initialPoolSize=5
c3p0.maxPoolSize=20
c3p0.acquireIncrement=2

# Timeouts / health checks
c3p0.checkoutTimeout=10000
c3p0.maxIdleTime=300
c3p0.idleConnectionTestPeriod=60
c3p0.preferredTestQuery=SELECT 1
c3p0.testConnectionOnCheckin=true

# Statement cache (optional)
c3p0.maxStatements=200
c3p0.maxStatementsPerConnection=20

Then your Java becomes minimal:

package org.kodejava.jdbc;

import com.mchange.v2.c3p0.ComboPooledDataSource;

import javax.sql.DataSource;

public final class DataSources {
    private static final ComboPooledDataSource ds = new ComboPooledDataSource(); // reads c3p0.properties

    public static DataSource c3p0() {
        return ds;
    }

    public static void shutdown() {
        ds.close();
    }
}

c3p0 will auto-load configuration from:

  • c3p0.properties on the classpath, or
  • c3p0-config.xml (more advanced, supports named configs)

3) Important usage rule: always close connections

With pooling, connection.close() does not close the physical DB connection; it returns it to the pool. So keep using try-with-resources:

try (Connection con = dataSource.getConnection();
     PreparedStatement ps = con.prepareStatement("SELECT * FROM t WHERE id = ?");
) {
  ps.setLong(1, 123L);
  // ...
}

4) Common pitfalls (quick checklist)

  • Don’t create a new ComboPooledDataSource per query. Create one and reuse it.
  • Set checkoutTimeout so you fail fast under load.
  • Use a validation query if your DB/network closes idle connections.
  • Tune pool sizes to match DB limits (max connections on the server) and your concurrency.

Maven dependencies

<dependencies>
  <dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.11.1</version>
  </dependency>

  <dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.7.7</version>
  </dependency>
</dependencies>

Maven Central Maven Central

How to Configure Apache DBCP Connection Pool in JDBC?

To configure an Apache DBCP connection pool for “plain” JDBC, you typically create a pooled DataSource once at startup, then get connections from it (and always close them to return to the pool).

Below are the two most common approaches.

1) Recommended (DBCP2): BasicDataSource (simplest)

Create and configure the pool

package org.kodejava.jdbc;

import org.apache.commons.dbcp2.BasicDataSource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public final class Dbcp2Example {
    public static DataSource dataSource() {
        BasicDataSource ds = new BasicDataSource();
        ds.setUrl("jdbc:postgresql://localhost:5432/app");
        ds.setUsername("<db_user>");
        ds.setPassword("<db_password>");

        ds.setMaxTotal(20);
        ds.setMaxIdle(10);
        ds.setMinIdle(2);

        ds.setValidationQuery("SELECT 1");
        ds.setTestOnBorrow(true);

        return ds;
    }

    public static void main(String[] args) throws Exception {
        DataSource ds = dataSource();
        try (Connection c = ds.getConnection();
             PreparedStatement ps = c.prepareStatement("SELECT 1");
             ResultSet rs = ps.executeQuery()) {
            while (rs.next()) System.out.println(rs.getInt(1));
        }
    }
}

Key idea: with pooling, conn.close() does not close the physical DB connection; it returns it to the pool.


2) Lower-level (more flexible): PoolingDataSource + Commons Pool

This approach wires DBCP to Commons Pool manually (useful when you want full control over the pool object/config).

package org.kodejava.jdbc;

import org.apache.commons.dbcp2.ConnectionFactory;
import org.apache.commons.dbcp2.DriverManagerConnectionFactory;
import org.apache.commons.dbcp2.PoolableConnection;
import org.apache.commons.dbcp2.PoolableConnectionFactory;
import org.apache.commons.dbcp2.PoolingDataSource;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import javax.sql.DataSource;

public final class ManualDbcpPoolFactory {

    public static DataSource createDataSource() {
        ConnectionFactory cf =
                new DriverManagerConnectionFactory("jdbc:postgresql://localhost:5432/app",
                        "<db_user>", "<db_password>");

        PoolableConnectionFactory pcf = new PoolableConnectionFactory(cf, null);
        pcf.setValidationQuery("SELECT 1");

        GenericObjectPoolConfig<PoolableConnection> config = new GenericObjectPoolConfig<>();
        config.setMaxTotal(20);
        config.setMaxIdle(10);
        config.setMinIdle(2);
        config.setTestOnBorrow(true);

        GenericObjectPool<PoolableConnection> pool = new GenericObjectPool<>(pcf, config);
        pcf.setPool(pool);

        return new PoolingDataSource<>(pool);
    }
}

Practical tuning checklist (what to set and why)

  • Sizing
    • maxTotal: hard cap of concurrent borrowed connections.
    • maxIdle / minIdle: how many connections to keep around to absorb spikes.
  • Validation
    • Use validationQuery (or validationQueryTimeout) and pick one strategy:
      • testOnBorrow=true (safer, slightly more overhead), or
      • testWhileIdle=true + eviction run (common for reducing borrow-time latency).
  • Timeouts
    • maxWaitMillis: how long callers wait for a free connection before failing.
  • Always close
    • Ensure try-with-resources everywhere; leaks will exhaust the pool.

Maven dependencies

<dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-dbcp2</artifactId>
        <version>2.14.0</version>
    </dependency>

    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
        <version>2.13.1</version>
    </dependency>

    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.7.7</version>
    </dependency>
</dependencies>

Maven Central Maven Central Maven Central