Connection Management & Pooling
Connection Management & Pooling
In a web application, database connections are a shared, finite resource. Every HTTP request that needs to hit the database must borrow a connection, do its work, and return the connection promptly. Get this wrong — either by leaking connections or by opening too many — and your application will grind to a halt under moderate load. This lesson shows you the professional patterns that prevent both failure modes.
Why Opening a Fresh Connection Every Time Is a Problem
Establishing a TCP connection to a database server involves a DNS lookup, a TCP three-way handshake, driver authentication, and session setup on the server side. On a LAN this commonly takes 20–100 ms; over a cloud network it can easily exceed 200 ms. For a web endpoint that must respond in under 500 ms, spending a third of its budget on connection setup is unacceptable.
Beyond latency, database servers enforce a hard limit on simultaneous sessions (MySQL defaults to 151; PostgreSQL to 100). A DriverManager-based application that opens a connection per request will saturate that limit under modest concurrency and throw SQLNonTransientConnectionException at everyone else.
The DataSource Interface
javax.sql.DataSource is the standard JDBC abstraction for a connection factory. Its contract is simple:
All pooling libraries (HikariCP, Apache DBCP2, c3p0, Tomcat JDBC Pool) implement this interface. Your application code only ever calls dataSource.getConnection() — it never knows or cares whether the returned Connection came from a pool, a fresh socket, or a test stub. That indirection is what makes the design testable and portable.
HikariCP — the Industry Standard Pool
HikariCP has been the default pool in Spring Boot since version 2.0 and is widely regarded as the fastest, most reliable JDBC pool available. Add it to your Maven project:
Create a singleton DataSource during application startup — in a Servlet-based application, a ServletContextListener is the right hook:
Any servlet or DAO can then retrieve the shared DataSource from the ServletContext:
conn.close() on a pooled connection, HikariCP does not close the underlying socket — it resets session state and returns the logical handle to the pool. Forgetting to call it is just as harmful as a real leak: the pool slot is reserved forever and the application eventually deadlocks waiting for a connection that never comes back.
Sizing the Pool Correctly
A common mistake is to set maximumPoolSize very high, reasoning that "more connections = more throughput". The opposite is usually true once you exceed the database server's optimal concurrency. The HikariCP team and PostgreSQL documentation both recommend starting with:
In a microservice world where multiple application instances share one database, the total connection count across all instances must remain within the server's max_connections. Ten instances each with a pool of 10 = 100 connections — right at the PostgreSQL default ceiling. Factor this in before bumping pool sizes.
JNDI — Container-Managed DataSources
In an application-server environment (WildFly, GlassFish, Payara, Tomcat in enterprise mode) you often don't create the pool in code at all. Instead the server administrator configures a pool in the server's management console and registers it under a JNDI (Java Naming and Directory Interface) name. Your application looks it up:
The JNDI resource reference must also be declared in web.xml (or @Resource injection in a Jakarta EE component):
With CDI or EJB you can inject the DataSource directly, which is even cleaner:
What Happens Inside the Pool
Understanding the lifecycle helps you debug problems:
- Borrow: Your code calls
ds.getConnection(). HikariCP picks an idle connection from the pool. If none is available and the pool is belowmaximumPoolSize, it opens a new one. If the pool is full, it waits up toconnectionTimeoutmilliseconds then throwsSQLTransientConnectionException. - Use: You execute SQL on the borrowed connection. If an exception causes you to skip
close(), the pool's housekeeping thread will eventually detect the leak (ifleakDetectionThresholdis configured) and log a warning. - Return:
conn.close()resets autoCommit, clears warnings, rolls back any uncommitted transaction, and marks the connection available. - Validation: Before handing a connection to the next caller, HikariCP runs a fast validation (
isValid()or yourconnectionTestQuery) to detect connections the database server closed due towait_timeoutor a network hiccup. - Eviction: Connections idle longer than
idleTimeout, or alive longer thanmaxLifetime, are quietly closed and replaced — keeping the pool fresh.
autoCommit=false and return a connection to the pool without calling commit() or rollback(), HikariCP will roll the transaction back on return — but the next borrower may see inconsistent state in row-level locks held until that rollback, causing mysterious timeouts. Use try-with-resources or a finally block that always calls rollback() on error and commit() on success.
Enabling HikariCP Metrics (Optional but Useful)
HikariCP exposes pool statistics via JMX and Micrometer out of the box. In a Spring Boot application you get pool metrics in Actuator (/actuator/metrics/hikaricp.connections) for free. In a standalone Servlet app you can poll programmatically:
Watch threadsAwaitingConnection in production. Sustained values above zero mean your pool is too small or your queries are running too long — both are actionable signals.
Summary
Connection pooling via DataSource is not an optimisation you add later — it is a correctness requirement for any web application. Use HikariCP (or a JNDI-managed pool in a full application server), size the pool empirically, always return connections promptly with try-with-resources, and monitor pool metrics so you catch saturation before it becomes an outage. In the next lesson you will put a pooled DataSource to work executing real CRUD statements with PreparedStatement.