A connection pool lends logical connection handles over a bounded collection of physical database connections; closing a borrowed handle ordinarily returns it to the pool.
Java connection pools: borrowed handles, rollback and admission timeout
Java 17+. The downloadable Maven fixture supplies HikariCP 5.1.0 and H2 2.3.232.
Keep the transaction with the borrower
The pool below has one maximum connection and a finite acquisition timeout. A caller owns the borrowed handle only for its operation. The handle must be closed on every path so another caller can borrow capacity; closing it is different from shutting down the pool itself.
The fixture disables auto-commit, creates its table, starts a data change and rolls that change back. A later borrower sees zero rows. That check is about transaction cleanup, not a throughput claim. The code calls rollback explicitly rather than depending on implicit pool cleanup to define business behavior.
The program also holds the only connection while attempting a second borrow. The timed rejection verifies that saturation is visible as an exception, rather than an unbounded wait. A larger pool does not automatically improve database throughput; it can move contention into the server.
Budget admission and the admitted operation
Connection acquisition timeout is not a statement execution timeout or a complete request deadline. A service needs a policy for each stage and a shutdown owner for the pool. Monitor active, waiting and timed-out borrowers under representative workload before choosing a production capacity.
Working program
import com.zaxxer.hikari.*;
import java.sql.*;
public class PooledReceiptBoundary {
public static void main(String[] args)throws Exception{
HikariConfig config=new HikariConfig();config.setJdbcUrl("jdbc:h2:mem:pool_"+java.util.UUID.randomUUID()+";DB_CLOSE_DELAY=-1");config.setMaximumPoolSize(1);config.setMinimumIdle(0);config.setConnectionTimeout(300);config.setAutoCommit(false);
try(HikariDataSource pool=new HikariDataSource(config)){
try(Connection connection=pool.getConnection()){
try(Statement sql=connection.createStatement()){sql.execute("CREATE TABLE receipts(id INT PRIMARY KEY)");}
try(PreparedStatement insert=connection.prepareStatement("INSERT INTO receipts VALUES(?)")){insert.setInt(1,17);insert.executeUpdate();}connection.rollback();
}
try(Connection held=pool.getConnection()){
try(Statement query=held.createStatement();ResultSet rows=query.executeQuery("SELECT COUNT(*) FROM receipts")){rows.next();System.out.println(rows.getInt(1));}
try(Connection unexpected=pool.getConnection()){throw new IllegalStateException("Second borrow admitted");}
catch(SQLTransientConnectionException saturated){System.out.println("pool timeout");}held.rollback();
}
try(Connection again=pool.getConnection()){System.out.println(again.isValid(1));again.rollback();}
}
}
}Output
0
pool timeout
trueCosts and boundaries
The pool retains at most the configured physical connections, but each connection also has driver and server-side state. Saturated acquisition waits up to its configured policy, subject to scheduling. This H2 fixture checks borrow/return, rollback and saturation; it does not size a production database pool.
Common Mistakes
- Closing a borrowed handle is not the same as closing the pool.
- Acquisition timeout does not bound the SQL statement.
- Do not retain a borrowed connection while waiting for unrelated work.
Read next
Session visibility, Admission accounting.
Apply this contract in Spring
Spring JdbcTemplate: bound values and visible database constraints. These lessons keep framework assembly separate from the Java contract.
