skip to content

Explain TransactionTemplate.execute(TransactionCallback) versus executeWithoutResult, and what TransactionCallback / TransactionStatus give you.

level: middleimportance: should knowfreq 40%

answer

  1. execute returns T; executeWithoutResult is void (5.2+)
  2. TransactionCallback.doInTransaction(status)
  3. TransactionCallbackWithoutResult = pre-5.2 void idiom
  4. TransactionStatus: setRollbackOnly / isNewTransaction / savepoints
  5. Checked exceptions must be wrapped in the callback

basics

~20 s

execute() takes a TransactionCallback that returns a value, and hands it a TransactionStatus you can use (e.g. to mark rollback). executeWithoutResult() is a Spring 5.2 convenience for void work — it takes a Consumer<TransactionStatus> so you don't return null.

solid answer

~30 s

execute(TransactionCallback<T>) runs your callback inside a transaction and returns the callback's result of type T; the callback receives a TransactionStatus. executeWithoutResult(Consumer<TransactionStatus>) is the void-returning convenience added in Spring 5.2 — before it you had to use TransactionCallbackWithoutResult or return null from execute. TransactionCallback is a single-method interface (doInTransaction(status)) that can throw RuntimeException. The TransactionStatus argument exposes control/inspection: status.setRollbackOnly() to force rollback without throwing, status.isNewTransaction() to see whether this call started the transaction or joined an existing one, and savepoint operations for nested transactions. Both variants share identical commit/rollback semantics; the only difference is whether you return a value.

code

java · 21 lines
java
// Returning a value with execute(...)
String name = txTemplate.execute(status -> {
    User u = userRepo.findById(id).orElseThrow();
    return u.getName(); // materialize inside the tx to avoid LazyInitializationException
});

// Void work with executeWithoutResult(...) (Spring 5.2+)
txTemplate.executeWithoutResult(status -> {
    auditRepo.record(id, "seen");
    if (!isAllowed(id)) {
        status.setRollbackOnly(); // roll back but still return normally
    }
});

// Pre-5.2 void idiom, still valid
txTemplate.execute(new org.springframework.transaction.support.TransactionCallbackWithoutResult() {
    @Override
    protected void doInTransactionWithoutResult(org.springframework.transaction.TransactionStatus status) {
        legacyRepo.touch(id);
    }
});

go deeper

for a junior

Know execute returns a value and executeWithoutResult is for void; both run the callback in a transaction.

for a middle

Explain TransactionCallback, the TransactionStatus you receive, and the pre-5.2 TransactionCallbackWithoutResult idiom.

for a senior

Discuss checked-exception wrapping and the LazyInitializationException pitfall of returning entities across the boundary.

for a principal

Set conventions: what may cross the callback boundary (DTOs vs entities), and when setRollbackOnly is preferable to throwing for control flow.

## The two methods `TransactionTemplate` exposes two callback entry points: ### 1. `execute(TransactionCallback<T>)` — returns a value ```java <T> T execute(TransactionCallback<T> action) throws TransactionException; ``` `TransactionCallback<T>` is a functional interface with one method: ```java @FunctionalInterface public interface TransactionCallback<T> { T doInTransaction(TransactionStatus status); } ``` Whatever the callback returns becomes the return value of `execute`. The callback may throw `RuntimeException` (or `Error`); doing so triggers rollback. ### 2. `executeWithoutResult(Consumer<TransactionStatus>)` — void Added in **Spring 5.2** for the common case where the work returns nothing: ```java void executeWithoutResult(Consumer<TransactionStatus> action) throws TransactionException; ``` Before 5.2 you had two options for void work: - return `null` from `execute` (ugly), or - use the abstract class `TransactionCallbackWithoutResult`, whose `doInTransactionWithoutResult(TransactionStatus)` you override. `executeWithoutResult` simply wraps your `Consumer` in a `TransactionCallbackWithoutResult` internally and returns `null`. It is pure ergonomics — semantics are identical to `execute`. ## TransactionStatus — the handle you're given Every callback receives a `TransactionStatus` (specifically a `DefaultTransactionStatus`). It lets you both **inspect** and **control** the current transaction: - `setRollbackOnly()` — mark the transaction so it will roll back at the end **without** you throwing an exception. Useful when you decide, based on business logic, that the work must not persist but you still want to return normally. - `isRollbackOnly()` — check whether rollback-only was already set (possibly by an inner participating transaction). - `isNewTransaction()` — `true` if this call actually started a new physical transaction, `false` if it joined an existing one (relevant with `PROPAGATION_REQUIRED`). - `isCompleted()` — whether the transaction has finished. - `hasSavepoint()`, `createSavepoint()`, `rollbackToSavepoint()`, `releaseSavepoint()` — savepoint support for nested transactions (`PROPAGATION_NESTED`). - `flush()` — flush the underlying session/store (e.g. JPA) if applicable. ## TransactionCallbackWithoutResult (the older void idiom) ```java txTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { repo.doWork(); } }); ``` Still valid, but `executeWithoutResult` with a lambda is the modern, terser form. ## Gotchas - The callback interface only declares `RuntimeException`; to propagate a **checked** exception you must wrap it (e.g. in a `RuntimeException`) — otherwise you cannot throw it directly from the lambda. This is a frequent stumbling point when calling APIs that throw checked exceptions inside the callback. - Returning a value that references lazily-loaded JPA entities *outside* the transaction can trigger `LazyInitializationException` — the transaction closes when `execute` returns, so materialize what you need inside the callback. - `setRollbackOnly()` and throwing both cause rollback, but only throwing propagates an error to the caller; `setRollbackOnly()` lets you return normally.

  • How do you propagate a checked exception thrown by code inside the callback?
    TransactionCallback.doInTransaction only declares RuntimeException, so you can't rethrow a checked exception directly. Wrap it in an unchecked exception (e.g. throw new IllegalStateException(checkedEx)) — which also triggers rollback since it's a RuntimeException. Alternatively catch it and call status.setRollbackOnly().
  • Why might reading a return value from execute() throw LazyInitializationException?
    The transaction (and JPA persistence context) is closed when execute() returns. If the returned object holds unfetched lazy associations and you access them afterward, there's no open session. Fetch/initialize everything you need inside the callback before returning.

saying these in an interview costs you the question

  • Saying executeWithoutResult existed since Spring 3 (it was added in 5.2)
  • Believing you can throw a checked exception directly from the TransactionCallback lambda
  • Confusing TransactionStatus.setRollbackOnly() with throwing — thinking the former also propagates an error to the caller

context