skip to content

How do you tune propagation, isolation, timeout and read-only on a TransactionTemplate for a specific unit of work, and what's the correct way to have multiple different settings?

level: seniorimportance: should knowfreq 35%

answer

  1. Template IS a DefaultTransactionDefinition
  2. setPropagationBehavior / setIsolationLevel / setTimeout / setReadOnly / setName
  3. One template per policy, configure once, reuse
  4. Never mutate a shared template per call (race)
  5. Isolation only applies to the tx that starts physical tx

basics

~20 s

TransactionTemplate is itself a TransactionDefinition, so you configure propagation, isolation, timeout, and read-only on the template with setters like setPropagationBehavior, setIsolationLevel, setTimeout, setReadOnly. For different settings, create separate template instances rather than mutating a shared one.

solid answer

~40 s

Because TransactionTemplate extends DefaultTransactionDefinition, its transaction settings live on the template itself: setPropagationBehavior (e.g. PROPAGATION_REQUIRES_NEW), setIsolationLevel (e.g. ISOLATION_READ_COMMITTED), setTimeout (seconds), setReadOnly(true), and setName. These are read each time execute() runs. The correct pattern for multiple policies is to create one pre-configured, immutable-after-setup template per policy — e.g. a requiresNewTemplate and a readOnlyTemplate — typically as Spring beans, and never mutate a shared template at call time. You can construct with the convenience constructor TransactionTemplate(txManager, definition) to copy an existing TransactionDefinition. Mutating a shared template's settings between calls is a thread-safety bug: concurrent callers would race on the definition. So per-call 'tuning' really means selecting the right pre-built template, not reconfiguring one on the fly.

code

java · 30 lines
java
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;

public class ReportService {

    private final TransactionTemplate requiresNewTx;
    private final TransactionTemplate readOnlyTx;

    public ReportService(PlatformTransactionManager tm) {
        // Independent tx for audit that must persist even if outer rolls back
        this.requiresNewTx = new TransactionTemplate(tm);
        this.requiresNewTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        this.requiresNewTx.setTimeout(5); // seconds

        // Read-only reporting query
        this.readOnlyTx = new TransactionTemplate(tm);
        this.readOnlyTx.setReadOnly(true);
        this.readOnlyTx.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        this.readOnlyTx.setName("monthlyReport");
    }

    public void audit(String msg) {
        requiresNewTx.executeWithoutResult(status -> auditRepo.write(msg));
    }

    public Report build() {
        return readOnlyTx.execute(status -> reportRepo.aggregate());
    }
}

go deeper

for a junior

Know the template has setters for propagation, isolation, timeout, read-only.

for a middle

Explain the meaning of each setting and the sensible defaults from DefaultTransactionDefinition.

for a senior

Advocate one-template-per-policy over mutating a shared template, and know isolation-on-join is ignored and read-only is a hint.

for a principal

Design a small set of named, injected templates as a transaction policy layer; reason about timeout budgeting and isolation/propagation interplay under contention.

## Where the settings live `TransactionTemplate` **extends `DefaultTransactionDefinition`**, which means the template *is* a `TransactionDefinition`. The tunable attributes are therefore properties of the template object: | Setter | Meaning | Example value | |---|---|---| | `setPropagationBehavior(int)` | How this unit relates to an existing transaction | `TransactionDefinition.PROPAGATION_REQUIRES_NEW` | | `setPropagationBehaviorName(String)` | Same, by name | `"PROPAGATION_NESTED"` | | `setIsolationLevel(int)` | DB isolation | `TransactionDefinition.ISOLATION_READ_COMMITTED` | | `setIsolationLevelName(String)` | Same, by name | `"ISOLATION_SERIALIZABLE"` | | `setTimeout(int)` | Max seconds before forced rollback | `10` | | `setReadOnly(boolean)` | Read-only hint (lets the tx manager/DB optimize) | `true` | | `setName(String)` | Transaction name (monitoring/logging) | `"reportExport"` | Defaults come from `DefaultTransactionDefinition`: `PROPAGATION_REQUIRED`, `ISOLATION_DEFAULT` (use the DB's default), `TIMEOUT_DEFAULT` (-1, none), `readOnly=false`. ## What each does - **Propagation** — controls joining vs. suspending vs. requiring-new. Common values: `REQUIRED` (join or start), `REQUIRES_NEW` (suspend outer, run independent tx), `NESTED` (savepoint within outer), `SUPPORTS`, `NOT_SUPPORTED`, `MANDATORY`, `NEVER`. - **Isolation** — `READ_UNCOMMITTED`, `READ_COMMITTED`, `REPEATABLE_READ`, `SERIALIZABLE`, or `DEFAULT`. Higher isolation reduces anomalies (dirty/non-repeatable/phantom reads) at the cost of concurrency. Note: isolation on `REQUIRES_NEW` starts a fresh physical tx so the level applies cleanly; setting a different isolation on a call that merely *joins* an existing transaction is not honored and, depending on the manager, can throw. - **Timeout** — measured in seconds; if the tx runs longer, it's rolled back. `-1` means the resource default (usually none). - **Read-only** — a hint. For JPA/Hibernate it can disable dirty checking and flush, and some drivers/DBs optimize accordingly. It is not a hard guarantee that writes are blocked. ## Correct pattern for multiple settings Because a template's definition is shared mutable state, you must **not** reconfigure one template per call in a concurrent app. Instead, build **one template per policy**, configure it once at startup, then reuse: ```java @Bean TransactionTemplate readOnlyTx(PlatformTransactionManager tm) { TransactionTemplate t = new TransactionTemplate(tm); t.setReadOnly(true); t.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); return t; } @Bean TransactionTemplate requiresNewTx(PlatformTransactionManager tm) { TransactionTemplate t = new TransactionTemplate(tm); t.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); t.setTimeout(5); return t; } ``` Then the call site *selects* the right template. This is what 'per-call definition tuning' means in practice — the template is configured up front, not at the moment of the call. ## Copying an existing definition The constructor `TransactionTemplate(PlatformTransactionManager tm, TransactionDefinition definition)` copies settings from any `TransactionDefinition` — handy to derive a template from an existing policy object. ## Gotchas - **Do not mutate a shared template between calls** — it's a data race (see the thread-safety question). Mutation is only acceptable during single-threaded setup. - **Isolation on a joining transaction is ignored** — only the transaction that actually starts the physical tx sets the level. Use `REQUIRES_NEW` if you truly need a different level mid-flow. - **Timeout counts total tx time**, including time your callback spends on non-DB work inside the transaction. - **read-only is a hint**, not enforcement — don't rely on it to prevent writes.

  • If you set ISOLATION_SERIALIZABLE on a template call that joins an existing PROPAGATION_REQUIRED transaction, does it take effect?
    No. Isolation is established when the physical transaction starts. A call that merely joins an existing transaction cannot change its isolation level; depending on the transaction manager it's ignored or even throws an IllegalTransactionStateException. To get a different isolation level mid-flow you must start a new physical transaction with PROPAGATION_REQUIRES_NEW.
  • What does setReadOnly(true) actually guarantee?
    It's a hint, not a hard guard. For Hibernate/JPA it can switch the flush mode to MANUAL and skip dirty checking, improving read performance; some drivers/databases pass it to the connection for optimization. It does not physically prevent writes — writes may still occur (and may fail or be silently unflushed depending on the provider).

saying these in an interview costs you the question

  • Mutating a single shared TransactionTemplate's settings at each call site in a multithreaded app
  • Assuming isolation set on a joining transaction is honored
  • Treating setReadOnly(true) as a write-blocking guarantee
  • Thinking timeout is in milliseconds (it's seconds)

context