JDBC in Depth: Connecting Java to Databases

    Atul Kabra3 min readUpdated

    JDBC (Java Database Connectivity) is the standard Java API for connecting to and operating on relational databases like PostgreSQL, MySQL, or Oracle. It gives you a uniform set of interfaces — Connection, Statement, PreparedStatement, ResultSet — so the same Java code works across databases by swapping a driver. Even when you later use Hibernate or Spring Data, those tools run on top of JDBC, so understanding it directly makes you a far stronger backend developer.

    The core flow

    Every JDBC interaction follows the same four steps:

    1. Get a connection to the database (via a URL, username, password, or a pool).
    2. Prepare a statement — ideally a PreparedStatement with placeholders.
    3. Execute the query and read the ResultSet (for SELECT) or an update count (for INSERT/UPDATE/DELETE).
    4. Close everything to release resources.

    Modern JDBC uses try-with-resources so closing happens automatically, even when exceptions occur.

    Always use PreparedStatement

    The single most important habit in JDBC is using PreparedStatement with parameter placeholders (?) instead of building SQL by concatenating strings. This prevents SQL injection — a critical security flaw — and lets the database cache the query plan.

    import java.sql.Connection;
    import java.sql.DriverManager;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    
    public class StudentDao {
    
        private static final String URL = "jdbc:postgresql://localhost:5432/lms";
    
        // Returns the email for a given student id, or null if not found
        public String findEmailById(long studentId) throws Exception {
    
            // Parameterised SQL — the ? is a placeholder, never string concatenation
            String sql = "SELECT email FROM students WHERE id = ?";
    
            // try-with-resources closes Connection, PreparedStatement, and ResultSet
            try (Connection conn = DriverManager.getConnection(URL, "app_user", "secret");
                 PreparedStatement ps = conn.prepareStatement(sql)) {
    
                // Bind the value safely to the first placeholder
                ps.setLong(1, studentId);
    
                try (ResultSet rs = ps.executeQuery()) {
                    if (rs.next()) {
                        return rs.getString("email"); // read by column name
                    }
                    return null; // no matching row
                }
            }
        }
    }
    

    Want to learn this properly?

    Join the waitlist for our courses — beginner-friendly, project-first classes in Jalgaon.

    Browse courses

    Transactions

    By default each statement auto-commits. When several statements must succeed or fail together (e.g. recording a payment and updating a balance), turn off auto-commit and control the transaction yourself:

    try (Connection conn = DriverManager.getConnection(URL, "app_user", "secret")) {
    
        conn.setAutoCommit(false); // start a manual transaction
    
        try (PreparedStatement debit = conn.prepareStatement(
                 "UPDATE accounts SET balance = balance - ? WHERE id = ?");
             PreparedStatement credit = conn.prepareStatement(
                 "UPDATE accounts SET balance = balance + ? WHERE id = ?")) {
    
            debit.setBigDecimal(1, amount);
            debit.setLong(2, fromId);
            debit.executeUpdate();
    
            credit.setBigDecimal(1, amount);
            credit.setLong(2, toId);
            credit.executeUpdate();
    
            conn.commit(); // both succeeded — make it permanent
        } catch (Exception e) {
            conn.rollback(); // anything failed — undo everything
            throw e;
        }
    }
    

    Connection pooling

    Opening a database connection is expensive. In any real application you do not call DriverManager.getConnection per request. Instead you use a connection pool (such as HikariCP) that keeps a set of reusable connections open. You then call dataSource.getConnection(), and "closing" it simply returns it to the pool. Spring Boot configures HikariCP for you automatically.

    Common mistakes

    • String-concatenating SQL with user input. This is the classic SQL injection hole. Always use PreparedStatement placeholders.
    • Not closing resources. Leaked connections exhaust the pool and crash the app. Use try-with-resources everywhere.
    • Calling getConnection() per request without a pool. It works in tutorials but collapses under real load.
    • Leaving auto-commit on for multi-step operations. Without an explicit transaction, a half-finished operation can corrupt data.
    • Reading columns by index when names are clearer. rs.getString("email") survives column reordering; rs.getString(1) is brittle.

    FAQ

    Do I need JDBC if I use Hibernate or Spring Data? Those frameworks use JDBC internally. Knowing it helps you debug connection, transaction, and performance issues the abstractions cannot fully hide.

    Which driver do I need? One that matches your database — for example the PostgreSQL JDBC driver for Postgres. You add it as a dependency and JDBC discovers it automatically.

    Keep learning

    JDBC is the layer beneath ORMs. See how Hibernate and JPA build on it, how a REST API exposes this data, or explore the Advanced Java hub.


    Want to master Java data access end to end? Join the waitlist for the Advanced Java course at Infoplanet, Jalgaon.

    Want to learn this properly?

    Join the waitlist for our courses — beginner-friendly, project-first classes in Jalgaon.

    Browse courses
    Atul Kabra

    Founder, Infoplanet

    Atul Kabra founded Infoplanet in 2001 and has spent over two decades teaching programming — C, C++, Java, databases and more — to students across Maharashtra.

    Related guides