skip to content

What happens to the `isolation` attribute when a `@Transactional` method joins an already-running transaction?

level: seniorimportance: should knowfreq 40%

answer

  1. Isolation set only at physical begin
  2. REQUIRED join -> inner isolation ignored, outer wins
  3. validateExistingTransaction=true -> IllegalTransactionStateException
  4. REQUIRES_NEW -> new connection -> isolation applies
  5. Self-invocation bypasses proxy entirely

basics

~20 s

Isolation is applied only when a new physical transaction starts. If the method uses the default PROPAGATION_REQUIRED and joins an existing transaction, its declared isolation is ignored — the outer transaction's level wins. You can make Spring throw instead by enabling validateExistingTransaction.

solid answer

~40 s

The isolation level is bound to the physical connection created when a transaction **begins**, so it only takes effect when a *new* physical transaction is started. Under the default `PROPAGATION_REQUIRED`, an inner method that joins the caller's existing transaction runs with the **outer** transaction's isolation, and its own declared `isolation` is **silently ignored**. This is a classic gotcha: `@Transactional(isolation = SERIALIZABLE)` on a helper called from within another transaction does nothing. To catch the mismatch, Spring's `AbstractPlatformTransactionManager` has a `validateExistingTransaction` flag (default false); when true, a participating transaction that requests a different isolation than the one in progress throws `IllegalTransactionStateException`. To actually get a different level you must force a separate physical transaction with `PROPAGATION_REQUIRES_NEW`, which suspends the outer transaction and starts a new connection where your isolation applies.

code

java · 16 lines
java
@Service
public class Orders {

    @Transactional // default REQUIRED, DB default isolation
    public void placeOrder() {
        audit();   // joins THIS transaction -> its isolation is ignored
    }

    // Force a separate physical tx so the level actually applies.
    @Transactional(propagation = Propagation.REQUIRES_NEW,
                   isolation   = Isolation.SERIALIZABLE)
    public void audit() {
        // Runs SERIALIZABLE on its own new connection,
        // independent commit/rollback from placeOrder().
    }
}

go deeper

for a junior

May not know propagation and isolation interact.

for a middle

Knows REQUIRES_NEW starts a new transaction but may miss that REQUIRED silently ignores inner isolation.

for a senior

Explains the join-ignores-isolation behavior and validateExistingTransaction.

for a principal

Designs transaction boundaries so isolation is declared where a physical transaction actually begins, and weighs REQUIRES_NEW's dual-connection cost.

## Why isolation and propagation interact **Propagation** decides whether a `@Transactional` method reuses an existing transaction or starts a new one. **Isolation** is a property that can only be set on a **physical** transaction/connection at the moment it begins. Put together: isolation is honored *only when a new physical transaction is created*. ## The default: PROPAGATION_REQUIRED With the default `Propagation.REQUIRED`, if a transaction is already active the method **joins** it (a logical inner transaction sharing the same physical connection). No new connection is opened, so there is no point at which `setTransactionIsolation` runs for the inner method. Result: **the inner method's declared isolation is ignored**, and it runs at the outer transaction's level. ```java @Transactional(isolation = Isolation.READ_COMMITTED) public void outer() { inner(); // same physical tx } @Transactional(isolation = Isolation.SERIALIZABLE) // IGNORED when called from outer() public void inner() { ... } ``` Called directly, `inner()` runs SERIALIZABLE; called from `outer()`, it runs READ_COMMITTED. Silent and surprising. ## Making the mismatch loud: validateExistingTransaction `AbstractPlatformTransactionManager` exposes `setValidateExistingTransaction(boolean)`, default **false**. When set **true**, if a participating (joining) transaction requests an isolation level that differs from the current transaction's, Spring throws `IllegalTransactionStateException` ("Participating transaction with definition [...] specifies isolation level which is incompatible with existing transaction"). It also validates read-only mismatches. This turns a silent bug into a fail-fast error — useful to enable in strict setups. ## Getting a genuinely different level: REQUIRES_NEW To actually run part of the work at a different isolation, force a **new physical transaction**: ```java @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) public void inner() { ... } ``` `REQUIRES_NEW` **suspends** the outer transaction, obtains a **new connection**, and begins a fresh physical transaction — so `setTransactionIsolation(SERIALIZABLE)` runs and applies. Note the costs: two concurrent connections are held, and the inner transaction commits/rolls back independently of the outer one. ## Related gotchas - **Self-invocation**: calling `inner()` via `this.inner()` bypasses the proxy entirely, so *no* new transaction (and no isolation) is applied regardless of propagation. Must go through the proxy/another bean. - **PROPAGATION_NESTED** uses a savepoint on the *same* connection, so it also cannot change isolation. - The isolation only re-applies where a physical begin happens: REQUIRED (when none exists), REQUIRES_NEW, and NOT_SUPPORTED/never affect it differently. ## Takeaway Declaring an isolation level on a method that will be *joined* into an outer transaction is a no-op. Either ensure that method is the transaction boundary, or use `REQUIRES_NEW`, and consider `validateExistingTransaction=true` to catch accidental mismatches early.

  • How do you make Spring fail loudly when an inner method requests a different isolation than the running transaction?
    Set `validateExistingTransaction = true` on the `AbstractPlatformTransactionManager`. Then a participating transaction whose isolation (or read-only flag) differs from the active one throws `IllegalTransactionStateException` instead of silently ignoring it.
  • Why does calling the annotated method from within the same class not apply its isolation?
    Because `@Transactional` works through a proxy; a `this.method()` self-invocation never crosses the proxy, so no new transaction advice runs at all — neither propagation nor isolation takes effect.

saying these in an interview costs you the question

  • Believing an inner @Transactional method's isolation always applies
  • Thinking PROPAGATION_NESTED can change isolation (it uses a savepoint on the same connection)
  • Assuming self-invocation triggers the isolation setting

context