skip to content

JDBC

The raw Java database API underneath every JVM ORM: DataSource and connection handling, PreparedStatement, ResultSet, and manual transaction and batch control. Interviewers ask about JDBC to check you can explain what Hibernate is doing on your behalf, and to see whether you close resources and bind parameters instead of concatenating SQL.

on this pageshow

questions

6

In JDBC, how should Connection, Statement and ResultSet be closed, and why?

level: juniorimportance: must knowfreq 70%

answer

  1. Something outside the JVM is being held
  2. The language has a construct for this
  3. Reverse order, and on every exception path
  4. Suppressed, not replaced, when close throws

basics

~20 s

Declare each of them in a try-with-resources block so they close in reverse order even when something throws. Every one holds a scarce handle — a server-side cursor or a pooled connection — and an unclosed handle is leaked until the process ends.

solid answer

~50 s

All three implement `AutoCloseable`, so the correct pattern is a single try-with-resources that declares the `Connection`, then the `PreparedStatement`, then the `ResultSet` (or nested blocks when the ResultSet comes from an execute call inside the body). Resources are closed in reverse declaration order automatically, on the success path and on every exception path, and an exception thrown by `close()` is attached to the primary exception as a suppressed exception rather than replacing it. The spec does say closing a `Statement` closes its current `ResultSet` and closing a `Connection` closes its statements, but leaning on that cascade is a bad habit: the cascade only fires when the outer object is actually closed, which is precisely what fails in leaky code. On a pooled `DataSource`, `Connection.close()` does not drop a socket — it hands the connection back to the pool, so *not* calling it removes that connection from circulation entirely.

code

java · 10 lines
java
String sql = "SELECT id, email FROM users WHERE active = ?";
try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setBoolean(1, true);
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            process(rs.getLong("id"), rs.getString("email"));
        }
    }
}   // rs closed, then ps, then conn returned to the pool

go deeper

for a junior

Write the try-with-resources shape from memory and know that all three JDBC objects belong in it. Be able to say what each one is holding on the database side.

for a middle

Explain reverse-order closing, suppressed exceptions, and the cascade rules — including the asymmetry that a ResultSet close does not close its Statement — and why depending on the cascade is still wrong.

for a senior

Talk about how leaks actually show up in a running service and how you find the borrow site, plus the loop case where a long-held connection accumulates cursors even though every method eventually returns.

for a principal

Own the convention: data-access code never hands JDBC objects across a boundary, leak detection is on in non-production environments, and the pattern is enforced by static analysis so it does not depend on each reviewer noticing.

## What is actually being held Each JDBC object corresponds to something scarce outside the JVM. A `Connection` is a session with the database server — a socket, server memory, and in a pooled application one of a strictly limited number of slots. A `Statement` may own a server-side cursor and prepared-statement state. A `ResultSet` may own an open cursor over rows the server is still holding. None of this is reclaimed by garbage collection in any timely way; drivers may implement finalizer-like safety nets, but they are not a design you can rely on. The consequence of forgetting is not an immediate error, which is what makes it dangerous. The application works in testing and degrades in production: statement handles accumulate until the database refuses new cursors, or borrowed connections are never returned and every request eventually blocks waiting for one. ## try-with-resources is the answer Since Java 7, `Connection`, `Statement` and `ResultSet` all extend `AutoCloseable`, so the language does the work: Declare the resources in one `try (...)` header, or in nested headers when a later resource is produced by a call rather than by a plain expression. Java closes them in the **reverse** of declaration order — ResultSet first, Connection last — which is the order you want, since a cursor should be released before the session that owns it. The close calls happen on normal completion, on `return`, and on any exception. Exception handling is the subtle part. If the body throws and a `close()` also throws, the body's exception is the one propagated and the close exception is attached to it; you can read them with `Throwable.getSuppressed()`. Hand-written `finally` blocks get this wrong in the opposite direction: a naive `finally { rs.close(); }` that throws replaces the real failure with a meaningless close error, hiding the original cause. That alone is a good reason never to hand-roll the pattern any more. Before Java 7 the correct shape needed a nested `try`/`finally` per resource with a null check in each, which is why so much legacy JDBC code is a wall of boilerplate — and why so much of it is subtly wrong. ## The cascade rules, and why not to depend on them JDBC does specify cascading closes. Closing a `Statement` closes the `ResultSet` currently open on it; re-executing a `Statement` also implicitly closes its previous `ResultSet`; closing a `Connection` releases its statements. Note the asymmetry: closing a `ResultSet` does **not** close the `Statement` that produced it. The cascades are real, but they only help when the outer object is genuinely closed — and the failure mode you are guarding against is exactly the outer object *not* being closed. There is also a middle case that bites in long-running code: a method that holds one `Connection` open for minutes and creates statements inside a loop leaks a cursor per iteration, because nothing closes until the connection finally does. Close each object at the scope where you stop needing it. ## Pooled connections change the meaning of close() When the `Connection` came from a pooled `DataSource`, the object you hold is a logical handle wrapping a physical connection. Calling `close()` on it does not tear down the network connection — it marks the handle dead and returns the physical connection to the pool for the next borrower. This inverts the intuition that closing is expensive and therefore worth avoiding: with a pool, closing is cheap and *skipping* it is what costs you, because that connection is removed from the pool's inventory for as long as your code holds it. Pools typically also reset per-connection state such as auto-commit when the connection goes back, so returning it promptly is what keeps the reset honest. ## What to check in review Look for any JDBC object created outside a try-with-resources header. Look for methods that return a `ResultSet` or a `Statement` to a caller, which makes ownership ambiguous — map rows into your own objects inside the method and return those instead. Look for a `Connection` stored in a field or held across a long computation. Look for a `finally` block whose own `close()` can throw. And in a service, enable the pool's leak-detection reporting during development: a stack trace naming the borrow site is far faster than reasoning about which path forgot to close.

  • What happens to an exception thrown by close() inside try-with-resources?
    If the block body already threw, the body's exception propagates and the close exception is attached to it as a suppressed exception, readable via Throwable.getSuppressed(). If the body completed normally, the close exception propagates on its own. A hand-written finally block would instead have discarded the original failure.
  • Why must you still close a Connection borrowed from a pool, when nothing is really being disconnected?
    Because close() is how the logical handle is returned to the pool. Skipping it takes that physical connection out of circulation for the lifetime of your reference, and the pool's per-return state reset never runs. Leak detection in the pool reports the borrow site when a handle is held too long.
  • Is it safe to return a ResultSet from a data-access method?
    No. The caller then owns a cursor whose statement and connection it did not create, and it cannot close them correctly. Consume the rows inside the method, map them into domain objects or a list, and return that; the JDBC objects stay inside the try-with-resources that created them.

saying these in an interview costs you the question

  • Relies on garbage collection to release connections
  • Closes only the Connection and calls it a day
  • Puts close() in a finally block that can throw and mask the cause
  • Thinks closing a ResultSet closes its Statement
  • Holds one Connection in a field for the object's lifetime

context

open as a page

In JDBC, what does PreparedStatement do that Statement does not, and why prefer it?

level: juniorimportance: must knowfreq 85%

basics

~20 s

PreparedStatement fixes the SQL text up front with ? placeholders and supplies values afterwards through numbered typed setters, so input is never parsed as SQL. The same object can be re-executed with new values and the parsed form reused.

open as a page

In JDBC, why do applications obtain connections from a DataSource rather than DriverManager?

level: middleimportance: must knowfreq 60%

basics

~20 s

DriverManager is a static factory that opens a brand-new physical connection per call from a URL hardcoded at the call site. DataSource is an interface whose implementation is configured elsewhere, which is what allows pooled, XA-capable or instrumented connections without changing application code.

open as a page

In JDBC, how do you make several statements commit or roll back as one transaction?

level: middleimportance: must knowfreq 72%

basics

~20 s

Call setAutoCommit(false) on the Connection, run the statements, then call commit() on success and rollback() in the catch block. There is no begin() method in JDBC: turning auto-commit off is what starts the unit of work.

open as a page

How do JDBC's addBatch and executeBatch work, and what does executeBatch return?

level: middleimportance: should knowfreq 50%

basics

~20 s

On a PreparedStatement you bind parameters and call addBatch() to buffer that parameter set, then executeBatch() sends the buffered commands together. It returns an int[] of update counts, one per command, where a value may be SUCCESS_NO_INFO when the count is unknown.

open as a page

A JDBC query over millions of rows OOMs the client before the first row is read. Why, and how do you stream it?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Most JDBC drivers buffer the entire result set into client memory when executeQuery returns, so ResultSet.next just walks that buffer. Streaming requires telling the driver to use a server-side cursor, which each driver gates behind its own preconditions.

open as a page