Explain the resource-suspension mechanics when REQUIRES_NEW is entered inside an existing transaction.
answer
- TransactionSynchronizationManager holds ThreadLocal ConnectionHolder
- suspend() -> SuspendedResourcesHolder -> doBegin new connection
- resume() rebinds after inner completes
- Two connections held -> pool deadlock
- Inner can't see outer's uncommitted writes/locks
basics
~20 sSpring unbinds the outer transaction's Connection from the thread (stored in TransactionSynchronizationManager), obtains a new Connection for the inner transaction, runs it, then rebinds and resumes the outer one after the inner commits or rolls back.
solid answer
~50 sWhen a REQUIRES_NEW method is entered while a transaction is active, `AbstractPlatformTransactionManager` detects the existing transaction and calls `suspend()`. Suspension takes the current resources — the `ConnectionHolder` (JDBC) or `EntityManagerHolder` (JPA) plus any registered `TransactionSynchronization`s bound to the thread via `TransactionSynchronizationManager` — and unbinds them, returning a `SuspendedResourcesHolder`. A fresh `Connection` is then acquired from the `DataSource` and bound for the inner transaction. The inner transaction runs, commits or rolls back on its own connection, and releases it. Finally the manager calls `resume()`, which rebinds the suspended resources so the outer transaction continues transparently. Two physical connections are therefore held simultaneously. This is why REQUIRES_NEW under load can exhaust a connection pool: if the pool is sized N and N threads each hold an outer connection while all wait for an inner one, you get a deadlock.
code
java · 15 lines// Conceptual view of what AbstractPlatformTransactionManager does
// for PROPAGATION_REQUIRES_NEW when a tx already exists:
if (definition.getPropagationBehavior() == PROPAGATION_REQUIRES_NEW
&& isExistingTransaction(transaction)) {
SuspendedResourcesHolder suspended = suspend(transaction); // unbind outer
try {
doBegin(transaction, definition); // NEW Connection bound to thread
// ... inner method body runs, then commit() or rollback() ...
} finally {
resume(transaction, suspended); // rebind outer, restore state
}
}
// Net effect during the inner body: outer Connection is checked out
// (idle) AND a second inner Connection is checked out => 2 at once.go deeper
Not expected to know internal suspend/resume mechanics.
Should know the outer is paused and a new connection is used.
Should describe TransactionSynchronizationManager, ConnectionHolder unbinding, and pool-deadlock risk.
Should reason about pool sizing math, lock visibility across connections, and JPA persistence-context implications.
## The thread-bound resource model Spring's transaction infrastructure binds transactional resources to the **current thread** using `TransactionSynchronizationManager`, an internal holder of `ThreadLocal` maps. For a JDBC/JPA transaction the key resource is the `DataSource` and the value is a `ConnectionHolder` (JDBC) or `EntityManagerHolder` (JPA) that wraps the active `Connection`/`EntityManager`. Any `TransactionSynchronization` callbacks (e.g. `afterCommit`) and flags like the current isolation level and read-only status are also bound to the thread. ## Entering REQUIRES_NEW `AbstractPlatformTransactionManager.getTransaction(...)` inspects the definition. For `PROPAGATION_REQUIRES_NEW`, if `isExistingTransaction(...)` returns true it does **not** join — instead it calls `suspend(transaction)`. ### suspend() `suspend()`: 1. Runs `suspend()` on all registered `TransactionSynchronization`s and clears them from the thread. 2. Calls `doSuspend(transaction)`, which (in `DataSourceTransactionManager`) unbinds the `ConnectionHolder` from the thread and nulls the connection reference on the transaction object. 3. Also captures thread state (transaction name, read-only flag, isolation, active flag). 4. Packages all of this into a `SuspendedResourcesHolder` and returns it. This holder is kept as a field on the new inner `DefaultTransactionStatus`. At this point the thread has **no** bound connection. ### Starting the inner transaction `doBegin(...)` runs for the inner transaction: it obtains a **new** `Connection` from the `DataSource`, sets autocommit off, applies isolation/read-only, wraps it in a new `ConnectionHolder`, and **binds** it to the thread. Now the thread again has a bound connection — a different physical one. **Two connections are checked out of the pool simultaneously.** ## Running and finishing The inner method executes. On normal return `commit()` (or on exception `rollback()`) runs against the inner connection, then the inner connection is released back to the pool. ### resume() After the inner transaction completes, `AbstractPlatformTransactionManager` calls `resume(transaction, suspendedResources)`: 1. `doResume(...)` rebinds the outer `ConnectionHolder` to the thread. 2. Thread state (name, read-only, isolation, active) is restored. 3. The suspended `TransactionSynchronization`s are re-registered and their `resume()` callbacks fire. The outer method then continues as if nothing happened. ## Consequences and gotchas 1. **Connection-pool pressure / deadlock.** Because the outer connection is *held* (not returned) during suspension, every nested REQUIRES_NEW doubles the peak connection demand of that call stack. With a pool of size N and N concurrent outer transactions each needing an inner connection, all N threads block waiting for a connection that will never free up → classic pool-exhaustion deadlock. Size the pool for the deepest simultaneous REQUIRES_NEW nesting. 2. **Transaction-manager support required.** The manager must implement `doSuspend`/`doResume`; standard `DataSourceTransactionManager` and `JpaTransactionManager` do. A manager that doesn't throws `TransactionSuspensionNotSupportedException`. 3. **Isolation / visibility.** The inner transaction on its own connection cannot see the outer transaction's uncommitted writes (they live on the suspended connection), and the outer cannot see the inner's uncommitted writes. After the inner commits, whether the outer sees the new rows depends on the outer's isolation level and whether it re-reads. 4. **Same-connection assumption breaks.** Code that assumes 'I'm all on one connection' (e.g. relying on a lock taken in the outer tx) will not have that lock visible inside the inner tx — the inner may block on a row the outer has locked, causing a real DB deadlock. 5. **JPA flush ordering.** With `JpaTransactionManager`, entering REQUIRES_NEW suspends the outer `EntityManager`; the inner uses a new persistence context. Pending changes in the outer context are *not* flushed into the inner transaction.
- Why can heavy use of REQUIRES_NEW deadlock a HikariCP pool that is sized too small?Each active outer transaction keeps its connection checked out while suspended, and the inner transaction needs a second connection. If every connection in the pool is held by an outer tx and they all wait for an inner connection, none can free up — deadlock. Size the pool above the peak simultaneous nesting depth.
- Can the inner REQUIRES_NEW transaction see rows the outer transaction just inserted but hasn't committed?No. The outer's uncommitted inserts live on the suspended connection; the inner runs on a different connection, so by normal isolation it cannot see them. It may even block if it tries to touch rows the outer has locked.
saying these in an interview costs you the question
- Claiming the outer connection is returned to the pool during suspension
- Saying suspend/resume swaps to the same connection
- Thinking the inner transaction sees the outer's uncommitted changes
- Assuming any transaction manager supports suspension