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

How do I use connection pooling in JDBC with HikariCP?

To use connection pooling in plain JDBC with HikariCP, the main shift is:

  • stop using DriverManager.getConnection(...) everywhere
  • create one DataSource (the pool) at startup
  • whenever you need a DB connection, call dataSource.getConnection()
  • always close resources with try-with-resources (closing returns the connection to the pool, it does not kill the physical connection)

1) Create a pooled DataSource once

A simple “factory” that builds a singleton pool:

package org.kodejava.jdbc;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import javax.sql.DataSource;
import java.time.Duration;

public final class DataSourceFactory {
    private static final HikariDataSource dataSource = create();

    private DataSourceFactory() {}

    private static HikariDataSource create() {
        HikariConfig config = new HikariConfig();

        config.setJdbcUrl("jdbc:postgresql://localhost:5432/app_db");
        config.setUsername("db_user");
        config.setPassword("db_password"); // use env vars/secret store in real apps

        // Pool sizing (tune per app + DB limits)
        config.setMaximumPoolSize(10);
        config.setMinimumIdle(2);

        // Timeouts
        config.setConnectionTimeout(Duration.ofSeconds(5).toMillis()); // wait for a connection from pool
        config.setIdleTimeout(Duration.ofMinutes(5).toMillis());
        config.setMaxLifetime(Duration.ofMinutes(30).toMillis());

        // Optional: validation / observability
        config.setPoolName("AppHikariPool");

        return new HikariDataSource(config);
    }

    public static DataSource getDataSource() {
        return dataSource;
    }

    /** Call this on application shutdown */
    public static void shutdown() {
        dataSource.close();
    }
}

Notes:

  • maximumPoolSize is usually the most important setting.
  • Prefer one pool per database, not one per DAO/class.

2) Use it in JDBC code (and always close)

Example query using the pooled DataSource:

package org.kodejava.jdbc;

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

public class UserRepository {
    private final DataSource dataSource;

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

    public String findEmailById(long id) throws SQLException {
        String sql = "select email from users where id = ?";

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

            ps.setLong(1, id);

            try (ResultSet rs = ps.executeQuery()) {
                return rs.next() ? rs.getString("email") : null;
            }
        }
    }
}

Key point: con.close() (done by try-with-resources) returns the connection to the pool.


3) Shutdown cleanly

If you’re writing a CLI app / desktop app / simple server, ensure the pool is closed on exit:

package org.kodejava.jdbc;

public class App {
    public static void main(String[] args) throws Exception {
        var ds = DataSourceFactory.getDataSource();
        var repo = new UserRepository(ds);

        System.out.println(repo.findEmailById(1L));

        DataSourceFactory.shutdown();
    }
}

For long-running apps, register a shutdown hook:

Runtime.getRuntime().addShutdownHook(new Thread(DataSourceFactory::shutdown));

4) Common configuration tips (practical)

  • Pool size: start with maximumPoolSize=10 for typical web apps, then tune using metrics and DB limits.
  • Don’t set minimumIdle too high unless you truly need warm connections.
  • Transactions: still work the same (use con.setAutoCommit(false) and commit/rollback), but make sure you always return the connection to the pool.
  • If you see “connection leak” warnings, it usually means some path didn’t close the connection (missing try-with-resources).

Maven dependencies

<dependencies>
  <dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>6.3.0</version>
  </dependency>

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

Maven Central Maven Central

How do I use savepoints in JDBC transactions?

Savepoints in JDBC provide fine-grained control over transactions by allowing you to roll back to a specific point within a transaction rather than undoing everything. This is particularly useful for handling optional operations or partial failures.

Key Steps to Use Savepoints

  1. Disable Auto-commit: Savepoints only work within a manual transaction.
  2. Set a Savepoint: Use connection.setSavepoint() to mark a logical point in your execution.
  3. Rollback to Savepoint: If an error occurs, use connection.rollback(savepoint).
  4. Release or Commit: Release the savepoint once it’s no longer necessary (though commit or a full rollback will also clear them).

Implementation Example

Here is how you can implement this in your project.

package org.kodejava.jdbc;

import java.sql.*;

public class SavepointExample {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/your_database";
        String user = "root";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, user, password)) {
            // 1. Disable auto-commit
            conn.setAutoCommit(false);

            try (Statement stmt = conn.createStatement()) {
                // Execute a required operation
                stmt.executeUpdate("INSERT INTO orders (item, qty) VALUES ('Laptop', 1)");

                // 2. Set a savepoint before an "optional" or risky operation
                Savepoint savepoint1 = conn.setSavepoint("Savepoint1");

                try {
                    // Try an optional operation (e.g., updating a secondary table)
                    stmt.executeUpdate("INSERT INTO loyalty_points (user_id, points) VALUES (1, 100)");
                } catch (SQLException e) {
                    // 3. Roll back to the savepoint if the optional part fails
                    System.out.println("Optional operation failed, rolling back to savepoint.");
                    conn.rollback(savepoint1);
                }

                // 4. Commit the overall transaction
                conn.commit();
                System.out.println("Transaction committed successfully.");

            } catch (SQLException e) {
                // If the main operation fails, roll back everything
                conn.rollback();
                e.printStackTrace();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Things to Keep in Mind

  • Named vs. Unnamed Savepoints: conn.setSavepoint() returns an unnamed savepoint with a system-generated ID. conn.setSavepoint("Name") creates a named one which can be easier for debugging.
  • Release Savepoints: While not strictly mandatory in all drivers, calling connection.releaseSavepoint(savepoint) can help free up resources if you have many savepoints in a long-running transaction.
  • Driver Support: Most modern databases (MySQL, PostgreSQL, Oracle, SQL Server) support savepoints, but you can check programmatically using DatabaseMetaData.supportsSavepoints().
  • Transaction Scope: Once a transaction is committed or rolled back entirely, all associated savepoints are automatically released and become invalid.

How do I use transactions in JDBC?

Using transactions in JDBC is essential when you need to ensure that a group of SQL statements either all succeed or all fail together (maintaining Atomicity).

By default, a JDBC Connection is in auto-commit mode, meaning every single SQL statement is treated as its own transaction and committed immediately.

To manage transactions manually, follow these three main steps:

1. Disable Auto-Commit

The first step is to tell the connection not to commit automatically after every execution.

connection.setAutoCommit(false);

2. Perform Your Database Operations

Execute your SQL statements (inserts, updates, deletes). If any of these throw an exception, you should catch it to handle the failure.

3. Commit or Rollback

  • commit(): If everything went well, save the changes permanently.
  • rollback(): If an error occurred, undo all changes made since the last commit.

Basic Example

Here is a clean pattern using a try-with-resources block for the connection and a nested try-catch for the transaction logic:

try (Connection conn = DriverManager.getConnection(url, user, pass)) {
    // Step 1: Disable auto-commit
    conn.setAutoCommit(false);

    try (PreparedStatement pstmt1 = conn.prepareStatement("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
         PreparedStatement pstmt2 = conn.prepareStatement("UPDATE accounts SET balance = balance + 100 WHERE id = 2")) {

        // Execute operations
        pstmt1.executeUpdate();
        pstmt2.executeUpdate();

        // Step 3a: Commit changes
        conn.commit();
        System.out.println("Transaction committed successfully!");

    } catch (SQLException e) {
        // Step 3b: Rollback changes if something goes wrong
        conn.rollback();
        System.err.println("Transaction rolled back due to error.");
        e.printStackTrace();
    }
} catch (SQLException e) {
    e.printStackTrace();
}

Important Tips:

  • Always use rollback() in the catch block: If you don’t roll back on failure, the connection might hold onto locks or leave the session in an inconsistent state.
  • Savepoints: If you have a very long transaction and want to roll back only a part of it, you can use conn.setSavepoint().
  • Transaction Isolation: You can control how “isolated” your transaction is from others using conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE), though the default is usually enough for standard applications.

How do I set fetch size for large queries?

To set the fetch size for large queries in Java using JDBC, you use the setFetchSize(int rows) method on a Statement or PreparedStatement object.

This gives the JDBC driver a hint as to the number of rows that should be fetched from the database when more rows are needed. This is particularly useful for large result sets to avoid loading everything into memory at once or to reduce the number of network round-trips.

Using JDBC Statement

Here is how you can apply it to a standard Statement:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

// ... existing code ...
try (Connection conn = DriverManager.getConnection(url, user, password);
     Statement stmt = conn.createStatement()) {

    // Set the fetch size to 100 rows
    stmt.setFetchSize(100);

    try (ResultSet rs = stmt.executeQuery("SELECT * FROM very_large_table")) {
        while (rs.next()) {
            // Process rows
        }
    }
} catch (Exception e) {
    e.printStackTrace();
}

Important Considerations

  1. Driver Support: setFetchSize is a hint. Not all JDBC drivers honor this value in the same way.
  2. MySQL Specifics: By default, the MySQL driver fetches all rows into memory. To stream results (fetch row-by-row), you must set the fetch size to Integer.MIN_VALUE and use a forward-only, read-only result set:
    stmt.setFetchSize(Integer.MIN_VALUE);
    
  3. Oracle Specifics: Oracle has a default fetch size (usually 10). Increasing this to 100 or 500 can significantly improve performance for large queries.

  4. Memory vs. Network:
    • Small fetch size: Saves memory but increases network round-trips (slower).
    • Large fetch size: Reduces network round-trips (faster) but consumes more client-side memory.

Using Spring Data JPA / Jakarta EE

Since your project uses Spring Data JPA, you can also set the fetch size using the @QueryHints annotation on your repository methods:

import jakarta.persistence.QueryHint;
import org.springframework.data.jpa.repository.QueryHints;
import static org.hibernate.jpa.HibernateHints.HINT_FETCH_SIZE;

@QueryHints(value = @QueryHint(name = HINT_FETCH_SIZE, value = "100"))
List<User> findAllByStatus(String status);