skip to content

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

level: middleimportance: must knowfreq 72%

answer

  1. There is no begin() in this API
  2. A fresh connection already has a mode
  3. Commit last inside the try, roll back in catch
  4. One call both changes mode and commits

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.

solid answer

~50 s

A fresh JDBC `Connection` is in auto-commit mode, meaning every statement is its own committed transaction. To group statements you call `conn.setAutoCommit(false)`; JDBC has no `begin()` — the transaction implicitly starts with the next statement and lasts until you call `commit()` or `rollback()`. The canonical shape is a try block that runs the statements and ends with `commit()`, and a catch block that calls `rollback()` before rethrowing. Two pitfalls matter. Calling `commit()` while auto-commit is still on throws a `SQLException`, and calling `setAutoCommit(true)` while a transaction is in flight commits it — so you never use that call as a way to abandon work. For partial rollback there are savepoints: `Savepoint sp = conn.setSavepoint()`, then `conn.rollback(sp)` undoes only the statements after that point and leaves the transaction open. Isolation is set with `setTransactionIsolation` before the transaction begins.

code

java · 22 lines
java
try (Connection conn = dataSource.getConnection()) {
    conn.setAutoCommit(false);
    try (PreparedStatement debit = conn.prepareStatement(
             "UPDATE account SET balance = balance - ? WHERE id = ?");
         PreparedStatement credit = conn.prepareStatement(
             "UPDATE account 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();
    } catch (SQLException e) {
        conn.rollback();
        throw e;
    } finally {
        conn.setAutoCommit(true);
    }
}

go deeper

for a junior

Recall that auto-commit is on by default and that grouping statements means setAutoCommit(false), commit on success, rollback on failure. Be able to write that block without help.

for a middle

Explain the state machine: no begin exists, commit while auto-commit is on throws, switching auto-commit back on commits an active transaction, and connection state must be restored before a pooled connection is returned.

for a senior

Show judgment about scope — locks and row versions held for the transaction's whole life — and be able to place savepoints correctly, including why a savepoint per loop iteration signals a misplaced boundary.

for a principal

Own the policy: where transaction boundaries live in the architecture, how long-running work is decomposed into idempotent steps instead of long transactions, and how isolation levels are chosen and verified per database target.

## Auto-commit is the default, and it is a transaction mode A `Connection` handed to you by a driver starts with auto-commit **on**. That is not "no transactions" — it means the driver commits after each individual statement, so every `executeUpdate` is its own tiny transaction. Two updates that must both succeed or both fail cannot be expressed this way: if the process dies between them, the first is already durable. Switching modes is the only "begin" JDBC offers. `conn.setAutoCommit(false)` says that from now on statements accumulate until you decide. The transaction itself starts implicitly with the next statement that touches the database, and ends only when you call `commit()` or `rollback()` — or when the connection is closed, in which case the outcome is driver-dependent and should never be relied on. ## The canonical shape Turn auto-commit off, run the statements, commit as the last thing inside the try, and roll back in the catch before rethrowing. Everything about that ordering is deliberate. `commit()` is the final statement in the happy path so that any exception from any earlier step reaches the catch with nothing committed. The catch rolls back and then rethrows, because swallowing the exception after rolling back turns a failure into a silent no-op. The rollback itself can throw — a broken connection is a common reason — so it is worth guarding it so the original exception is not lost. If the connection came from a pool, restore `setAutoCommit(true)` before returning it, or make sure your pool is configured to reset it. Connection state is per-connection, not per-borrower, and a connection returned in manual-commit mode will surprise the next piece of code that assumes the default. ## Two traps in the state machine First: calling `commit()` or `rollback()` while the connection is still in auto-commit mode throws a `SQLException`. There is nothing to commit, and the specification makes it an error rather than a silent no-op precisely because such a call signals confused intent. Second, and nastier: calling `setAutoCommit(true)` during an active transaction **commits** it. Some developers reach for that call in a cleanup path thinking it resets the connection; it durably writes exactly the half-finished work they were trying to discard. Roll back first, then restore the mode. A related caveat: many database engines commit implicitly when DDL executes, so a schema statement in the middle of your transaction can end it without any JDBC call being involved. Keep DDL out of application transactions. ## Savepoints Savepoints give partial rollback inside a transaction. `conn.setSavepoint()` returns an unnamed `Savepoint`; `conn.setSavepoint("name")` returns a named one. `conn.rollback(savepoint)` undoes only the work performed after that savepoint was set, leaving the transaction open and everything before it intact, so you can carry on and commit. `conn.releaseSavepoint(savepoint)` discards the marker when you no longer need it; a savepoint is also invalidated by a commit or a full rollback, and using an invalid one throws. The legitimate use is an optional step inside a mandatory unit of work — insert the order, set a savepoint, try to attach an optional enrichment, roll back to the savepoint if that fails, commit the order regardless. What savepoints are not is a general error-recovery mechanism: databases differ in how much they support, they cost server resources, and code that sets one per loop iteration is usually describing a transaction boundary it drew in the wrong place. ## Isolation level `conn.setTransactionIsolation(int)` takes one of the constants on `Connection`: `TRANSACTION_READ_UNCOMMITTED`, `TRANSACTION_READ_COMMITTED`, `TRANSACTION_REPEATABLE_READ`, `TRANSACTION_SERIALIZABLE`, or `TRANSACTION_NONE`. Set it before the transaction starts; changing it mid-transaction is not portable. `DatabaseMetaData.supportsTransactionIsolationLevel(int)` reports what the database actually offers, and drivers may silently map an unsupported level onto a stronger one — so "I set it" and "it is in force" are different claims. As with auto-commit, a level set on a pooled connection outlives your use of it unless it is reset. ## Scope discipline The hardest part of manual transactions is not the API but the scope. A transaction holds locks and, on MVCC engines, keeps old row versions alive for as long as it is open — so a transaction that spans a network call to another service, a user's think time, or a large in-memory computation is a production incident waiting to happen. Acquire the connection, do the database work, commit, release. If work needs to be split, design idempotent steps rather than stretching one transaction across them.

  • What happens if you call setAutoCommit(true) while a transaction is in progress?
    The transaction is committed. The specification requires it, so using that call as a cleanup or reset step durably writes the work you were trying to abandon. Roll back explicitly first, then restore auto-commit — usually in a finally block before a pooled connection goes back.
  • How do you change the isolation level, and why might setting it not be enough?
    Call conn.setTransactionIsolation with one of the Connection constants before the transaction starts. It may not take effect as written: databases support different subsets, drivers can map an unsupported level to a stronger one, and DatabaseMetaData.supportsTransactionIsolationLevel is what tells you which levels are real for that target.
  • When is a savepoint the right tool rather than a sign of bad transaction boundaries?
    When an optional step sits inside a mandatory unit of work and its failure must not discard the rest — attaching an enrichment to a record that has to be written either way. If you find yourself setting a savepoint per loop iteration, the loop body is really its own transaction and should be one.

saying these in an interview costs you the question

  • Looks for a begin() method on Connection
  • Uses setAutoCommit(true) to abandon a transaction
  • Commits inside the try and rolls back nowhere
  • Leaves a pooled connection in manual-commit mode
  • Keeps a transaction open across a remote service call

context