A third-party library calls dataSource.getConnection() itself and its writes aren't rolling back with your @Transactional method. What's happening and how do you fix it?
answer
- raw getConnection -> new Connection, auto-commit ON
- JdbcTemplate uses DataSourceUtils; library doesn't
- TransactionAwareDataSourceProxy routes via DataSourceUtils
- proxy close() is no-op inside tx
- TM/JdbcTemplate on RAW ds, proxy only to legacy code
basics
~20 sThe library opens its own Connection instead of the thread-bound one, so it runs outside your transaction with auto-commit on. Wrap the real DataSource in TransactionAwareDataSourceProxy and give the library that, so its getConnection() returns the transactional Connection.
solid answer
~40 sDataSourceTransactionManager only makes writes transactional when they run on the Connection it bound to the thread. Spring's own JdbcTemplate fetches that Connection through DataSourceUtils.getConnection, but a third-party library calling dataSource.getConnection() directly gets a fresh Connection from the pool with auto-commit on — its statements commit immediately and are invisible to your rollback. The clean fix is TransactionAwareDataSourceProxy: wrap the physical DataSource in it and hand the proxy to the library. Its getConnection() returns a proxy Connection that transparently delegates to DataSourceUtils, so it resolves to the thread-bound transactional Connection when one exists and a normal one otherwise; close() is also made a no-op inside a transaction. Keep Spring's JdbcTemplate/transaction manager wired to the *raw* DataSource, and expose only the wrapped proxy to code that insists on calling getConnection itself.
code
java · 26 lines@Configuration
public class DataConfig {
@Bean
public DataSource dataSource() { // the real pooled DataSource
return buildHikari();
}
// Transaction manager and Spring's JdbcTemplate target the RAW DataSource.
@Bean
public DataSourceTransactionManager txManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
// Only the legacy library gets the transaction-aware proxy.
@Bean
public LegacyReportEngine reportEngine(DataSource dataSource) {
DataSource txAware = new TransactionAwareDataSourceProxy(dataSource);
return new LegacyReportEngine(txAware); // its getConnection() now joins the tx
}
}go deeper
Recognize that a library opening its own Connection isn't in your transaction.
Name TransactionAwareDataSourceProxy as the fix and that JdbcTemplate uses DataSourceUtils.
Wire it correctly (proxy to legacy, raw to TM) and explain the no-op close semantics.
Judge whether the library should be enrolled at all, weigh connection-hold/lock cost, and know XA is needed for multi-resource atomicity.
## Root cause: two ways to get a Connection Under `DataSourceTransactionManager`, exactly one `Connection` is bound to the thread (auto-commit off) and registered in `TransactionSynchronizationManager` keyed by the `DataSource`. There are then two very different ways code obtains a Connection: - **Transaction-aware path**: `DataSourceUtils.getConnection(ds)` → checks the thread, returns the *bound* Connection. This is what `JdbcTemplate`, `NamedParameterJdbcTemplate`, MyBatis' Spring integration, and Hibernate's Spring-managed connection provider all use. These participate in the transaction. - **Raw path**: `dataSource.getConnection()` → asks the pool for *any* Connection. Fresh Connections come with **auto-commit = true**, so each statement commits on its own and is neither part of your transaction nor subject to your rollback. A third-party library that manages its own JDBC (some batch tools, reporting engines, legacy DAOs) uses the raw path — hence the symptom: your `@Transactional` rolls back, but the library's inserts persist. ## The fix: TransactionAwareDataSourceProxy `org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy` wraps a target `DataSource`. Its `getConnection()` returns a **proxy `Connection`** whose method calls are routed through `DataSourceUtils`: - If a transactional Connection is bound to the thread, the proxy delegates every statement to it → the library now runs *inside* your transaction. - `close()` on the proxy becomes a no-op while a transaction is active (the real close happens when the transaction manager commits/rolls back), so the library's `try/finally { con.close(); }` doesn't prematurely release the shared Connection. - Outside any transaction it behaves like a plain DataSource (new Connection, real close). ### Critical wiring rule Give the **proxy** only to the legacy/third-party code. Wire your `DataSourceTransactionManager` (and ideally your own `JdbcTemplate`) to the **raw/target** DataSource. Reason: the transaction manager must operate on the underlying physical DataSource so the binding key and the actual `commit()`/`rollback()` land on the real Connection. Spring's docs explicitly note TransactionAwareDataSourceProxy should be the *outermost* wrapper handed to code that can't be made Spring-aware, and that Spring-managed components (JdbcTemplate, the transaction manager) should target the native DataSource directly. (If you point the transaction manager at the proxy, you can get double-wrapping and subtle bugs.) ## Other escape hatches to recognize (same root cause) - **Different DataSource bean**: the binding is keyed by DataSource identity; a JdbcTemplate on another DataSource won't join. - **New thread** (`@Async`, executor, reactive scheduler): ThreadLocal binding does not propagate; work runs in no/new transaction. - **Self-invocation / final/private methods**: the proxy that starts the transaction is bypassed, so nothing is ever bound. - **New Connection from `getConnection()` for a needed *separate* transaction**: sometimes this is *desired* (e.g., writing an audit row that must survive a rollback) — then leaving it on the raw DataSource, or using propagation `REQUIRES_NEW`, is the right design, not a bug. ## Design judgment (principal-level) Before reaching for the proxy, ask whether the library *should* be in your transaction. If it must be atomic with your writes → `TransactionAwareDataSourceProxy` (single DataSource) or, across resources, XA/JTA. If it is genuinely independent (fire-and-forget logging) → leave it raw or give it its own DataSource. Avoid making everything transaction-aware reflexively; it couples the library's connection lifecycle to your transaction and can hold the shared Connection (and its locks) longer than expected, hurting throughput.
- Why should the transaction manager point at the raw DataSource, not the proxy?The manager must bind and commit the underlying physical Connection. Pointing it at the proxy risks double-wrapping and lifecycle confusion. The proxy is meant only for code that calls getConnection() itself and can't use DataSourceUtils.
- When is it correct to leave code on the raw DataSource so it escapes the transaction?When the write must be independent of the outer transaction — e.g. audit/log rows that must persist even if business logic rolls back. Then a separate Connection (or propagation REQUIRES_NEW) is the intended design.
- Does TransactionAwareDataSourceProxy help across two databases?No. It only makes code join the single thread-bound transaction for that one DataSource. Cross-database atomicity still needs JTA/XA.
saying these in an interview costs you the question
- Claiming any getConnection() automatically joins the current transaction.
- Wiring the transaction manager to the TransactionAwareDataSourceProxy instead of the raw DataSource.
- Assuming the proxy provides cross-resource (multi-DB) atomicity.
- Reflexively wrapping everything transaction-aware without considering held-Connection/lock duration.