skip to content

Walk through what DataSourceTransactionManager does to the JDBC Connection on begin, commit, and rollback.

level: middleimportance: should knowfreq 45%

answer

  1. doBegin: getConnection, save+clear autoCommit, bind
  2. doCommit -> Connection.commit()
  3. doRollback -> Connection.rollback(), RuntimeException by default
  4. cleanup: restore autoCommit, releaseConnection
  5. NESTED=savepoint, REQUIRES_NEW=second Connection

basics

~20 s

On begin it gets a Connection, saves its auto-commit setting, and sets auto-commit false. On success it calls Connection.commit(); on a runtime exception it calls Connection.rollback(). Then it restores auto-commit and returns the Connection to the pool.

solid answer

~40 s

Begin (doBegin): the manager obtains a Connection from the DataSource, remembers the original auto-commit flag, calls setAutoCommit(false) if needed, applies isolation/read-only hints, wraps it in a ConnectionHolder, and binds it to the thread. Commit (doCommit): after the method returns normally the transaction interceptor calls commit, which invokes Connection.commit() on the bound Connection. Rollback (doRollback): if the method throws an exception that triggers rollback — by default any RuntimeException or Error, not checked exceptions unless rollbackFor says so — it calls Connection.rollback(). Cleanup (doCleanupAfterCompletion): it unbinds the resource from the thread, restores the original auto-commit value, resets isolation/read-only, and releases the Connection via DataSourceUtils.releaseConnection back to the pool. For NESTED propagation it uses JDBC Savepoints on the same Connection instead of a full new transaction.

code

java · 24 lines
java
@Service
public class InvoiceService {
    private final JdbcTemplate jdbc;
    public InvoiceService(JdbcTemplate jdbc) { this.jdbc = jdbc; }

    // Checked exceptions do NOT roll back unless you say so.
    @Transactional(rollbackFor = BillingException.class)
    public void bill(long id) throws BillingException {
        jdbc.update("UPDATE invoice SET status='SENT' WHERE id=?", id);
        if (!gatewayOk(id)) {
            throw new BillingException("gateway declined"); // -> Connection.rollback()
        }
        // normal return -> Connection.commit()
    }

    // Forcing rollback without rethrowing:
    @Transactional
    public void tryUpdate(long id) {
        jdbc.update("UPDATE invoice SET note='x' WHERE id=?", id);
        if (shouldAbort()) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }
}

go deeper

for a junior

Know begin sets auto-commit off; success commits, exception rolls back.

for a middle

Explain the default rollback rule and cleanup restoring auto-commit/isolation.

for a senior

Distinguish NESTED savepoints vs REQUIRES_NEW second Connection and setRollbackOnly.

for a principal

Reason about rollback-only propagation, UnexpectedRollbackException, and Connection-hold/isolation cost across nested calls.

## Lifecycle stages (mapped to real methods) `DataSourceTransactionManager` extends `AbstractPlatformTransactionManager` and implements these template hooks: ### 1. doBegin — starting the transaction - Calls `dataSource.getConnection()` (once) to get a physical `Connection`. - Reads and stores the Connection's current `getAutoCommit()`; if it is `true`, calls `setAutoCommit(false)`. **Auto-commit = true means every SQL statement is its own committed transaction** — turning it off is what lets multiple statements form one transaction. - Applies transaction attributes: `Connection.setTransactionIsolation(...)` if `@Transactional(isolation=...)` differs from default; `setReadOnly(true)` for read-only hints (a hint to the driver/DB, e.g. enabling optimizations). - Wraps the Connection in a `ConnectionHolder`, marks it 'synchronized with transaction,' and calls `TransactionSynchronizationManager.bindResource(dataSource, holder)` — binding it to the current thread. - Records a timeout deadline if `@Transactional(timeout=...)` is set. ### 2. Business execution All `DataSourceUtils.getConnection(dataSource)` lookups return the bound Connection (see the connection-sharing question). Statements accumulate but nothing is committed yet. ### 3a. doCommit — normal return - The interceptor calls the manager's `commit`. Before the physical commit, any registered `TransactionSynchronization` callbacks run (`beforeCommit`, `beforeCompletion`). - Calls `Connection.commit()` — atomically persisting all buffered changes. - `afterCommit`/`afterCompletion(STATUS_COMMITTED)` callbacks fire. ### 3b. doRollback — exception path - Triggered when the method throws an exception the rollback rules cover. **Default rule**: roll back on `RuntimeException` and `Error`; **commit** on checked exceptions. Override via `@Transactional(rollbackFor = SomeCheckedException.class)` or `noRollbackFor`. - Calls `Connection.rollback()`, discarding all buffered changes. - If an inner participating transaction failed and set rollback-only, an outer commit throws `UnexpectedRollbackException`. ### 4. doCleanupAfterCompletion — always - Unbinds the `ConnectionHolder` from the thread. - **Restores** the original `autoCommit` value and resets isolation/read-only so the Connection is clean for the next pool user. - Calls `DataSourceUtils.releaseConnection(con, dataSource)`, which actually closes/returns the physical Connection to the pool now that the transaction is over. ## Propagation nuances on the same Connection - `REQUIRED` (default): joins an existing transaction or starts one. A nested inner method uses the *same* Connection; if it fails it can mark the whole transaction rollback-only. - `NESTED`: does **not** open a new Connection — it creates a JDBC `Savepoint` on the current Connection. Rolling back the inner block rolls back to the savepoint, keeping the outer transaction alive. Requires driver savepoint support. - `REQUIRES_NEW`: **suspends** the current transaction (its ConnectionHolder is set aside) and begins a genuinely new one on a *second* Connection; the two commit/roll back independently. ## Common gotchas - **Checked exceptions don't roll back by default** — a surprising number of bugs come from throwing a checked exception and seeing data committed. Use `rollbackFor`. - **Swallowed exceptions**: catching the exception inside the @Transactional method means it returns normally → commit. To force rollback without rethrowing, call `TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()`. - **Read-only** is a hint; it does not guarantee the DB rejects writes. - Setting auto-commit false and forgetting to commit (in manual, non-Spring JDBC) leaks open transactions — Spring handles this for you, which is the point.

  • Does a checked exception roll back a DataSourceTransactionManager transaction by default?
    No. The default rule rolls back only on RuntimeException and Error. Checked exceptions commit unless you declare @Transactional(rollbackFor=...) or call setRollbackOnly().
  • How does NESTED propagation differ from REQUIRES_NEW at the Connection level?
    NESTED sets a JDBC Savepoint on the same Connection (inner rollback returns to the savepoint). REQUIRES_NEW suspends the current transaction and opens a second Connection that commits independently.

saying these in an interview costs you the question

  • Saying checked exceptions roll back by default.
  • Thinking catching the exception still rolls back (it commits unless setRollbackOnly).
  • Believing REQUIRES_NEW reuses the same Connection (it uses a new one).

context