☕ Java

Connection

java.sql.Connection represents a single session with a specific database — the live link through which all statements, transactions, and metadata queries flow, and the factory from which Statement, PreparedStatement, and CallableStatement objects are created. A Connection is a relatively expensive, stateful resource that wraps an underlying network socket and server-side session, which is why its lifecycle, transaction settings, and proper closing matter so much in practice. This entry covers obtaining and closing connections, transaction control (auto-commit, commit, rollback, savepoints, isolation levels), creating statements from a connection, and connection metadata and health checks.

Obtaining, Using, and Closing a Connection

A Connection is obtained either from DriverManager.getConnection() or, in production code, from a DataSource's getConnection() method, and it represents a stateful, resource-backed session — it holds an open network socket to the database server, server-side session state, and any in-progress transaction. Because of this cost, Connection objects should be held only as long as necessary and always explicitly closed; failing to close a connection leaks the underlying socket and server-side session resources, and in a long-running server this kind of leak will eventually exhaust the connection pool or the database's max-connections limit, causing cascading failures elsewhere in the system. Since Connection implements AutoCloseable, the idiomatic and strongly recommended pattern in modern Java is try-with-resources, which guarantees close() is called even if an exception is thrown while the connection is in use — manually calling close() in a finally block is the older equivalent pattern and is more error-prone to get right (forgetting null-checks, or closing in the wrong order relative to Statement and ResultSet objects derived from the connection). A Connection obtained from a pool-backed DataSource behaves the same way from the application's point of view: calling close() on it does not actually tear down the physical socket, but returns the connection to the pool for reuse, which is precisely why this resource-management discipline matters even more in pooled environments — a "leaked" connection there silently shrinks the pool's available capacity rather than immediately erroring out.
Java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class ConnectionLifecycle {
    public static void main(String[] args) {
        String url = "jdbc:postgresql://localhost:5432/storedb";

        // try-with-resources: Connection, PreparedStatement, and ResultSet
        // are all AutoCloseable and are closed automatically, in reverse
        // order of creation, even if an exception is thrown
        try (Connection conn = DriverManager.getConnection(url, "app_user", "secret");
             PreparedStatement ps = conn.prepareStatement(
                     "SELECT id, name FROM products WHERE category = ?")) {

            ps.setString(1, "electronics");
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    System.out.println(rs.getLong("id") + ": " + rs.getString("name"));
                }
            }

        } catch (SQLException e) {
            // Connection (and any derived statements/result sets still open)
            // are guaranteed closed by this point, even though we landed here
            System.err.println("Database error: " + e.getMessage());
        }

        // ── The older, more error-prone manual pattern, for contrast ───────
        Connection conn = null;
        try {
            conn = DriverManager.getConnection(url, "app_user", "secret");
            // ... use conn ...
        } catch (SQLException e) {
            System.err.println("Database error: " + e.getMessage());
        } finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException ignored) {
                    // closing failures are usually safe to log and ignore
                }
            }
        }
    }
}

Transaction Control — Auto-Commit, Commit, Rollback, and Savepoints

By default, a JDBC Connection operates in auto-commit mode, meaning every individual SQL statement is automatically wrapped in and committed as its own transaction the moment it completes — convenient for simple one-off statements but unsuitable whenever multiple statements need to succeed or fail together as a single atomic unit (e.g. debiting one account and crediting another). Calling conn.setAutoCommit(false) switches the connection into manual transaction mode, after which statements accumulate inside an open transaction until the application explicitly calls conn.commit() to make all the changes permanent, or conn.rollback() to discard them entirely and restore the pre-transaction state. Within a manually-managed transaction, Savepoint objects (created via conn.setSavepoint()) allow rolling back to an intermediate point inside the transaction rather than all the way to the start, which is useful for handling partial failures in a multi-step operation without abandoning everything already done successfully. Connection also exposes transaction isolation level control via setTransactionIsolation(), with standard levels (TRANSACTION_READ_UNCOMMITTED, TRANSACTION_READ_COMMITTED, TRANSACTION_REPEATABLE_READ, TRANSACTION_SERIALIZABLE) trading off consistency guarantees against concurrency and performance; not every database supports every level, and getMetaData().supportsTransactionIsolationLevel() can be used to check support before relying on a stricter level. Connection pools generally expect auto-commit to be restored to true (or otherwise reset) before a connection is returned to the pool, since the next caller to borrow that connection should not inherit an unexpected transaction state — most pooling libraries handle this automatically, but it's a common source of subtle bugs when bypassing the pool's intended usage pattern.
Java
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Savepoint;

public class TransactionExample {
    public static void transferFunds(Connection conn, long fromId, long toId, java.math.BigDecimal amount)
            throws SQLException {
        boolean originalAutoCommit = conn.getAutoCommit();
        conn.setAutoCommit(false);  // start a manual transaction

        Savepoint beforeDebit = null;
        try {
            beforeDebit = conn.setSavepoint("beforeDebit");

            try (PreparedStatement debit = conn.prepareStatement(
                    "UPDATE accounts SET balance = balance - ? WHERE id = ?")) {
                debit.setBigDecimal(1, amount);
                debit.setLong(2, fromId);
                debit.executeUpdate();
            }

            try (PreparedStatement credit = conn.prepareStatement(
                    "UPDATE accounts SET balance = balance + ? WHERE id = ?")) {
                credit.setBigDecimal(1, amount);
                credit.setLong(2, toId);
                credit.executeUpdate();
            }

            conn.commit();  // both updates become permanent together

        } catch (SQLException e) {
            if (beforeDebit != null) {
                conn.rollback(beforeDebit);  // undo only the debit, if it happened
            } else {
                conn.rollback();  // undo everything in this transaction
            }
            throw e;
        } finally {
            conn.setAutoCommit(originalAutoCommit);  // restore prior state before returning to pool
        }
    }

    public static void demonstrateIsolation(Connection conn) throws SQLException {
        if (conn.getMetaData().supportsTransactionIsolationLevel(
                Connection.TRANSACTION_SERIALIZABLE)) {
            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
        } else {
            conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
        }
    }
}

Creating Statements, Reading Metadata, and Checking Connection Health

Connection is the factory for every kind of executable statement: createStatement() returns a plain Statement for executing static SQL with no parameters, prepareStatement(sql) returns a PreparedStatement for parameterized SQL that the database can pre-compile and reuse across executions (and which protects against SQL injection when used correctly), and prepareCall(sql) returns a CallableStatement for invoking stored procedures and functions. Overloads of these factory methods also accept ResultSet type and concurrency constants, controlling whether a resulting ResultSet is scrollable and/or updatable rather than the default forward-only, read-only behavior. conn.getMetaData() returns a DatabaseMetaData object describing the capabilities and structure of the connected database itself — supported SQL features, the list of tables, column definitions, primary/foreign key information, the database product name and version — which is primarily used by tools (IDEs, ORMs, schema migration frameworks) rather than typical application code. For health checking, conn.isValid(timeoutSeconds) asks the driver to verify the connection is still usable within the given timeout, which connection pools use internally to evict dead connections before handing them out; conn.isClosed() merely reports whether close() has been called locally and does not by itself guarantee the underlying network connection is actually still alive, since a connection can go stale due to network issues, server-side timeouts, or database restarts without close() ever having been called on the Java side.
Java
import java.sql.*;

public class ConnectionFactoryAndMetadata {
    public static void main(String[] args) throws SQLException {
        try (Connection conn = DriverManager.getConnection(
                "jdbc:postgresql://localhost:5432/storedb", "app_user", "secret")) {

            // ── Three kinds of statements, all created from Connection ─────
            try (Statement plain = conn.createStatement()) {
                ResultSet rs = plain.executeQuery("SELECT COUNT(*) FROM products");
                rs.next();
                System.out.println("Product count: " + rs.getInt(1));
            }

            try (PreparedStatement ps = conn.prepareStatement(
                    "SELECT name FROM products WHERE id = ?")) {
                ps.setLong(1, 42);
                try (ResultSet rs = ps.executeQuery()) {
                    if (rs.next()) System.out.println(rs.getString("name"));
                }
            }

            try (CallableStatement cs = conn.prepareCall("{call recalc_inventory(?)}")) {
                cs.setLong(1, 42);
                cs.execute();
            }

            // Scrollable, updatable ResultSet variant
            try (Statement scrollable = conn.createStatement(
                    ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE)) {
                ResultSet rs = scrollable.executeQuery("SELECT id, name FROM products");
                if (rs.last()) {
                    System.out.println("Last row id: " + rs.getLong("id"));
                }
            }

            // ── Metadata about the database/driver itself ──────────────────
            DatabaseMetaData meta = conn.getMetaData();
            System.out.println(meta.getDatabaseProductName() + " " + meta.getDatabaseProductVersion());
            System.out.println("Driver: " + meta.getDriverName() + " " + meta.getDriverVersion());

            // ── Health check, the kind pools run before lending a connection ──
            boolean alive = conn.isValid(2);  // 2-second timeout
            System.out.println("Connection healthy: " + alive);
        }
    }
}

Related Topics in Java Database Connectivity (JDBC)

JDBC Architecture
JDBC (Java Database Connectivity) is the standard Java API for connecting to and executing operations against relational databases, defined as a set of interfaces in java.sql and javax.sql that database vendors implement with concrete driver classes. Its architecture is deliberately layered to decouple application code from any specific database product: applications code against interfaces like Connection, Statement, and ResultSet, while a pluggable driver underneath translates those calls into the vendor-specific network protocol the database actually understands. This entry covers the two-layer (API/driver) architecture, the four historical driver types, the role of the JDBC driver manager and connection pooling layers, and how the pieces fit together at runtime.
JDBC Drivers
A JDBC driver is the vendor-specific library that implements the java.sql/javax.sql interfaces for a particular database product, translating standard JDBC calls into that database's native network protocol. Choosing, loading, registering, and configuring the correct driver is a prerequisite for any JDBC code to run, and the mechanics of how a driver gets discovered and instantiated have evolved significantly since JDBC's introduction. This entry covers driver registration via the Service Provider Interface (SPI) versus the older explicit Class.forName() approach, JDBC URL syntax and structure, driver-specific configuration properties, and how to choose and manage driver dependencies in a real project.
DriverManager
java.sql.DriverManager is the original, classic entry point into JDBC: a static factory class that maintains a registry of available Driver implementations and hands back a live Connection when given a JDBC URL and credentials. It predates the javax.sql.DataSource abstraction and connection pooling, and while it remains fully functional and is still commonly used for simple applications, scripts, and tests, production server-side code generally prefers DataSource-based connection pooling for the reasons covered below. This entry covers how DriverManager locates and selects a driver, its core methods, login timeout and logging configuration, and where it fits (and doesn't fit) in modern application architecture.
Statement
java.sql.Statement is the base JDBC interface for sending SQL to a database and retrieving results, representing the simplest way to execute a static SQL string with no parameters. It is created from a Connection and supports three execution styles depending on whether the SQL returns rows, modifies data, or could be either, plus batch execution for running many statements efficiently in one round trip. This entry covers the executeQuery/executeUpdate/execute distinction, batch processing, and why Statement's lack of parameterization makes it unsuitable for most application code that handles user input, motivating PreparedStatement instead.