skip to content

How does a JDBC Connection illustrate state-dependent behavior, and what happens when you call methods after closing it?

level: middleimportance: must knowfreq 62%

answer

  1. Open vs. closed = primary state; transaction = sub-state
  2. After close(): methods throw SQLException
  3. close() is idempotent; isClosed() just queries state
  4. Pooled close() returns to pool, not a physical teardown
  5. try-with-resources drives the open→closed transition

basics

~20 s

A JDBC Connection is either open or closed. While open you can run queries; once you call close(), the same methods become illegal and throw SQLException. So the connection's behavior depends on its open/closed state.

solid answer

~50 s

A java.sql.Connection is a textbook example of state-dependent behavior. It moves through states: open and usable, then closed after close() is called. While open you can createStatement(), prepareStatement(), commit(), and rollback(). Once closed, calling almost any of those methods throws SQLException because the underlying physical (or pooled) resource is gone. There is also a transaction sub-state: with autocommit off, the connection is mid-transaction until you commit() or rollback(), and those operations only make sense in that state. isClosed() lets you query the open/closed state, and close() is idempotent — calling it on an already-closed connection is a no-op. The JDBC API doesn't expose a literal State pattern class hierarchy to you, but conceptually the driver implements exactly this: the same method behaves differently (succeeds vs. throws) based on the connection's internal state, which is why try-with-resources and pooling matter so much.

code

java · 13 lines
java
try (Connection conn = dataSource.getConnection()) {   // OPEN state
    conn.setAutoCommit(false);                          // enter transaction sub-state
    try (PreparedStatement ps =
             conn.prepareStatement("INSERT INTO orders(id) VALUES (?)")) {
        ps.setLong(1, 42);
        ps.executeUpdate();
        conn.commit();                                  // legal only mid-transaction
    } catch (SQLException e) {
        conn.rollback();                                // discard the in-progress work
        throw e;
    }
} // try-with-resources -> close(): OPEN -> CLOSED
// conn.createStatement() here would throw SQLException: connection is closed

go deeper

for a junior

Knows a connection is open or closed and that using it after close() throws SQLException.

for a middle

Explains both the open/closed state and the autocommit/transaction sub-state, and uses try-with-resources for the transition.

for a senior

Adds idempotent close(), isClosed() vs isValid(), pooling semantics (proxy as state machine), and savepoint sub-states.

for a principal

Reasons about pool sizing, leak detection, transaction boundaries across layers, and how the driver's internal state machine affects reliability and failover.

## Background: what JDBC and a Connection are **JDBC** (Java Database Connectivity) is Java's standard API for talking to relational databases. The central object is `java.sql.Connection`, which represents a single session to the database. You obtain one (from `DriverManager` or, in real apps, a connection **pool** like HikariCP), use it to run SQL, then release it. ## A Connection is a state machine A `Connection` is a clear real-world example of *state-dependent behavior*: the **same methods behave differently depending on the connection's internal state**. The main states: - **Open / active** — the connection is usable. You may call `createStatement()`, `prepareStatement()`, `setAutoCommit()`, `commit()`, `rollback()`, etc. - **Closed** — after you call `close()`. The session and its physical socket (or its slot in the pool) are released. ### What changes between states While **open**, the operations work normally. Once **closed**, *almost every* method on the connection throws `java.sql.SQLException` with a message like "connection is closed." This is precisely state-dependent behavior: `createStatement()` succeeds in one state and fails in another, with no change to the method call itself. The one well-behaved exception is `close()` itself, which is **idempotent**: closing an already-closed connection is defined to be a no-op (it does nothing rather than throwing). And `isClosed()` is a *query* method that simply reports which state you are in (note: `isClosed()` returning `false` does not guarantee the connection is still *valid* — the DB may have dropped it; use `isValid(timeout)` for a live check). ## The transaction sub-state There is a second, finer state axis: **transaction status**. - By default a connection is in **autocommit** mode — every statement commits immediately. - Call `setAutoCommit(false)` and the connection enters a **transaction-in-progress** state: statements accumulate until you `commit()` (make them permanent) or `rollback()` (discard them). `commit()` and `rollback()` only make sense in this mid-transaction state. Calling them is governed by the connection's current transaction state, again illustrating behavior that depends on internal state. Savepoints add even finer sub-states within a transaction. ## Why this matters in practice Because method legality depends on state, two practices are essential: 1. **try-with-resources** — `try (Connection c = ds.getConnection()) { ... }` guarantees `close()` runs (transitioning to closed) even on exception, preventing leaks. `Connection` implements `AutoCloseable` exactly so the language can manage this transition. 2. **Connection pooling** — `close()` on a *pooled* connection usually does **not** tear down the physical socket; instead it returns the wrapper to the pool and marks the handle closed. The pool's proxy is itself a small state machine: the same physical connection is open-to-the-pool but closed-to-you. ## Connecting back to the State pattern The JDBC API doesn't hand you a `State` interface with `OpenState`/`ClosedState` classes — that's an internal driver concern. But conceptually the driver *is* implementing state-dependent behavior: a flag (open/closed, in-transaction) gates which operations are legal, and the same call produces success or `SQLException` based on that flag. It's the ideal example to cite when explaining *why* the State pattern exists, even though the public API is a flat interface. ## Common pitfalls - Using a connection after `close()` → `SQLException`. - Forgetting `commit()` with autocommit off → work silently lost on close/rollback. - Assuming `isClosed()==false` means healthy → it doesn't; use `isValid()`. - Calling `commit()` in autocommit mode → throws (it's meaningless there).

  • Why is close() on a JDBC Connection idempotent while createStatement() is not?
    close() is defined to be a no-op on an already-closed connection so cleanup code (finally blocks, pools) can call it safely without guarding. createStatement() needs a live session, which no longer exists once closed, so it must throw rather than silently fail.
  • What's the difference between isClosed() and isValid()?
    isClosed() reports whether close() has been called on this handle — a cheap local flag check that can return false even if the database has dropped the link. isValid(timeout) actually checks the connection is still usable, e.g. by round-tripping to the DB, so it detects dead-but-not-closed connections.

saying these in an interview costs you the question

  • Saying a closed connection can be reopened by calling some reset method — there is no such thing; you obtain a new connection.
  • Believing close() throws on an already-closed connection — it is a no-op.
  • Assuming pooled close() always tears down the socket — usually it just returns the handle to the pool.
  • Treating isClosed()==false as proof the connection is alive.

context