How does DataSourceTransactionManager make multiple JdbcTemplate calls share one transaction?
answer
- ThreadLocal mailbox keyed by DataSource
- bindResource(ds, connectionHolder)
- JdbcTemplate -> DataSourceUtils.getConnection
- releaseConnection: no close if managed
- raw getConnection() escapes the tx
basics
~10 sIt gets one Connection, turns off auto-commit, and binds it to the current thread. JdbcTemplate asks DataSourceUtils for a Connection, which returns that same bound one, so every call runs in the same transaction.
solid answer
~40 sWhen a @Transactional method begins, DataSourceTransactionManager acquires a Connection from the DataSource, sets auto-commit off, and stores it in a ConnectionHolder bound to the thread through TransactionSynchronizationManager.bindResource. JdbcTemplate never calls dataSource.getConnection() directly — it calls DataSourceUtils.getConnection(dataSource), which checks the thread for a bound holder and returns that Connection. So the first, second, and Nth JdbcTemplate statements in the method all execute on one physical Connection and are part of the same transaction. When the method returns, the manager commits that Connection and unbinds it; on exception it rolls back. Because the binding is a ThreadLocal, this works transparently across DAO/service layers on the same thread with no need to pass a Connection around manually. Raw dataSource.getConnection() would bypass this and get a separate, non-transactional Connection.
code
java · 22 lines@Service
public class OrderService {
private final JdbcTemplate jdbc;
private final DataSource dataSource;
public OrderService(JdbcTemplate jdbc, DataSource dataSource) {
this.jdbc = jdbc;
this.dataSource = dataSource;
}
@Transactional
public void demo() {
// (1) Uses the thread-bound Connection -> inside the transaction.
jdbc.update("INSERT INTO audit(msg) VALUES ('a')");
// (2) Correct way to touch the SAME transactional Connection manually:
Connection shared = DataSourceUtils.getConnection(dataSource);
// (3) WRONG: a brand-new Connection, auto-commit ON, NOT in the tx.
// try (Connection rogue = dataSource.getConnection()) { ... } // its writes won't roll back
}
}go deeper
Know that JdbcTemplate reuses one Connection so all statements commit together.
Explain the ThreadLocal binding, DataSourceUtils lookup, and why release doesn't close.
Diagnose escapes: raw getConnection, wrong DataSource, thread switch, self-invocation.
Design around it: TransactionAwareDataSourceProxy, multi-DataSource keying, thread-confinement implications for async/reactive.
## The core trick: thread-bound resources Spring's declarative transactions rely on **binding a `Connection` to the current thread** and having data-access code look it up there. The two moving parts: 1. **`TransactionSynchronizationManager`** — a set of `ThreadLocal` maps. When a transaction starts, `DataSourceTransactionManager.doBegin()` calls `TransactionSynchronizationManager.bindResource(dataSource, connectionHolder)`. The *key* is the `DataSource` instance and the *value* is a `ConnectionHolder` wrapping the live `Connection` plus a reference count. 2. **`DataSourceUtils`** — the static bridge every Spring JDBC component uses. `DataSourceUtils.getConnection(dataSource)` first asks `TransactionSynchronizationManager.getResource(dataSource)`; if a `ConnectionHolder` is bound it returns its `Connection` (incrementing the ref count). Only if nothing is bound does it call `dataSource.getConnection()` and, if a transaction synchronization is active, register the new Connection. ## Why JdbcTemplate 'just works' `JdbcTemplate.execute(...)` obtains its Connection via `DataSourceUtils.getConnection(getDataSource())` and releases it via `DataSourceUtils.releaseConnection(con, getDataSource())`. `releaseConnection` **does not close** the Connection if it belongs to a managed transaction — it just decrements the ref count. So: - Call #1 → gets the bound Connection, runs UPDATE, 'releases' (no close because it's transactional). - Call #2 → gets the *same* bound Connection. - ...method returns → the transaction manager commits once and physically releases the Connection back to the pool. This is the whole mechanism behind 'all statements in a @Transactional method commit or roll back together.' It requires no code to pass Connections between methods, and it spans service→repository→another-repository as long as they run on the **same thread** and use the **same DataSource**. ## setAutoCommit and savepoints `doBegin` records the original auto-commit value, sets it to `false`, and restores it in `doCleanupAfterCompletion`. If propagation `NESTED` is used, the manager uses JDBC `Savepoint`s on that same Connection rather than a new one. ## Common ways to break the sharing - **Calling `dataSource.getConnection()` directly** (or a library that does): you get a *different* Connection with auto-commit on — its writes commit immediately and won't roll back with the transaction. Fix: use `JdbcTemplate`/`DataSourceUtils`, or wrap the DataSource in `TransactionAwareDataSourceProxy` so even direct `getConnection()` returns the bound one. - **Different `DataSource` instance**: the binding key is the DataSource object, so a JdbcTemplate built on a *different* DataSource won't see the bound Connection. All participants must share one DataSource bean. - **Switching threads** (`@Async`, manual executor, reactive scheduler): ThreadLocal binding does not follow to another thread, so the work runs outside the transaction. - **Self-invocation**: calling a @Transactional method from within the same bean bypasses the proxy, so no transaction begins and there is nothing bound. ## Mental model Think of `TransactionSynchronizationManager` as a thread-local mailbox keyed by DataSource, `DataSourceTransactionManager` as the one who puts the Connection in the mailbox and later commits it, and `DataSourceUtils` as the postman every JdbcTemplate uses to fetch from that mailbox.
- Why does DataSourceUtils.releaseConnection sometimes not close the Connection?Because if the Connection participates in a managed transaction, closing it would end the transaction early. releaseConnection only decrements the holder's ref count and lets the transaction manager close it at commit/rollback.
- How would you make a legacy library that calls dataSource.getConnection() itself participate in the transaction?Wrap the real DataSource in a TransactionAwareDataSourceProxy and hand that to the library; its getConnection() returns a proxy that resolves to the thread-bound transactional Connection.
saying these in an interview costs you the question
- Believing each JdbcTemplate call opens its own Connection even inside a transaction.
- Thinking the sharing works across threads (ThreadLocal doesn't propagate).
- Assuming a JdbcTemplate on a different DataSource still joins the transaction.