cswork / libraryOpen practice workspace

Articles

Spring Transaction Propagation Interview Questions: REQUIRES_NEW, Rollbacks, and Proxy Traps

Prepare for Spring transaction propagation interviews with tested REQUIRES_NEW, rollback-only, proxy, savepoint, and connection-pool failure examples.

Spring transaction propagation interview questions become difficult when the code looks correct but the database outcome is surprising. An audit record disappears despite REQUIRES_NEW. A caught exception still prevents commit. A request times out even though neither SQL statement is slow.

The annotation matters only when the relevant transaction advice actually runs.

This article concentrates on propagation failures and the evidence needed to explain them.

Evidence boundary: official Spring documentation supports the framework rules below. No candidate report is used to predict an employer's questions. Sources were checked on September 15, 2026.

Start with the boundary, not the propagation name

Official framework fact: in Spring's default proxy mode, transaction advice intercepts external calls through the proxy. A method calling another method on the same object bypasses that interception. Using @Transactional.

Ask four questions when examining a snippet:

  • Is the target a Spring-managed bean, and is transaction management enabled?
  • Does this particular invocation cross its transaction proxy?
  • Is an existing transaction associated with the current execution context?
  • Which transaction manager and datasource own the work?

A self-call inside an already transactional outer method may still execute within the outer transaction. The missing behavior is the inner method's newly requested boundary. Saying “self-invocation means there is no transaction” is therefore too broad.

Official proxy limitation: private methods cannot be advised through ordinary proxies, and class-based proxies cannot override final methods. Interface and class proxies also have different method-visibility rules. Diagnose the actual proxy mechanism instead of treating every method carrying an annotation as transactional. Spring proxying mechanisms.

Experiment one: why the audit disappears

Our fixture has two independent tables, business_log(note) and audit_log(note), both initially empty. Each contains a single VARCHAR(30) column. There are no foreign keys or shared-row updates between them, so the experiment isolates transaction boundaries rather than lock contention.

The Spring-managed Outer bean contains these methods. Its jdbc field is an injected JdbcTemplate:

@Transactional
public void selfCallThenFail() {
    jdbc.update("INSERT INTO business_log VALUES ('outer')");
    this.selfAudit();
    throw new IllegalStateException("outer failure");
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void selfAudit() {
    jdbc.update("INSERT INTO audit_log VALUES ('inner')");
}

Call selfCallThenFail() from outside the bean through its proxy. Observed locally: both tables remain empty after the exception. The direct call to selfAudit() never creates the independent transaction requested by its annotation; both inserts belong to the outer transaction.

Now move the audit method to a separate Spring-managed Inner bean and inject that bean into Outer. The relevant methods become:

// On the injected Inner bean:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void independentAudit() {
    jdbc.update("INSERT INTO audit_log VALUES ('inner')");
}

// On the Outer bean:
@Transactional
public void proxyCallThenFail() {
    jdbc.update("INSERT INTO business_log VALUES ('outer')");
    inner.independentAudit();
    throw new IllegalStateException("outer failure");
}

Observed locally: the business table has zero rows, but the audit table has one. The inner proxy establishes its own transaction and commits the audit before the outer method fails.

These are service-method excerpts, not a complete application. Our harness enables annotation-driven transactions, registers both beans, supplies JdbcTemplate, and uses this manager:

@Bean
PlatformTransactionManager transactionManager(DataSource ds) {
    return new DataSourceTransactionManager(ds);
}

To reproduce the distinction, call the bean obtained from the application context, catch the exception outside that call, and then query both tables. A direct new Outer(...) call would test a different setup. Use a fresh database for each case so an earlier successful audit cannot disguise a later failure.

REQUIRED: catching an exception does not clear rollback-only

Official propagation fact: REQUIRED starts a transaction when necessary and otherwise participates in the existing physical transaction. Participating logical scopes can affect its rollback-only status. Spring transaction propagation.

In the next experiment, the outer method inserts a business row and calls a separate bean's REQUIRED method. The inner method inserts an audit row and throws IllegalStateException. The outer method catches it and returns normally.

Observed locally: the caller receives UnexpectedRollbackException, and both tables contain zero rows. The inner transaction interceptor saw the unchecked exception and marked the shared transaction for rollback. Catching it in the outer method changed Java control flow but did not restore the transaction's ability to commit.

The location of the catch matters. If code catches a plain application exception entirely inside one transactional method, the interceptor may never see it. That is different from catching an exception after another transactional proxy has processed it. Database errors can also leave provider-specific failure state; swallowing an exception is not a reliable recovery strategy.

A useful interview answer identifies the first boundary that observes the failure and the boundary that attempts the final commit. “The exception was handled” does not answer either question.

REQUIRES_NEW and NESTED produce different guarantees

Official propagation fact: REQUIRES_NEW uses an independent physical transaction, while NESTED generally uses savepoints within one physical transaction. Support depends on the transaction manager and resource. Propagation semantics.

We tested three related cases with separate proxied beans:

CasePersisted result
Inner REQUIRES_NEW fails; outer catchesBusiness 1, audit 0
Inner NESTED fails; outer catchesBusiness 1, audit 0
Inner NESTED succeeds; outer later failsBusiness 0, audit 0

The first two results look identical, but the boundaries differ. In the nested case, rollback to the savepoint removes the inner insert while preserving the earlier outer insert. The surviving work still depends on the final outer commit. Successful nested work has not earned an independent commit.

In the independent case, an inner commit can survive a later outer rollback, as the first experiment showed. But REQUIRES_NEW does not swallow exceptions. Observed locally: when an inner unchecked failure escapes through the outer method, both transactions roll back under the tested default rules.

Manager-specific fact: DataSourceTransactionManager supports nested transactions through JDBC savepoints. Do not generalize that result to every ORM or transaction manager; a savepoint does not automatically restore arbitrary in-memory entity state. Manager documentation.

Experiment two: the connection pool is part of correctness

For the connection experiment, the outer method inserts a business row, calls the independent audit method, and returns successfully. We change only the Hikari pool capacity between runs:

config.setMaximumPoolSize(1);
config.setMinimumIdle(1);
config.setConnectionTimeout(300);

The timeout is expressed in milliseconds. Official pool behavior: when the pool has reached its maximum and no idle connection is available, a borrower waits up to connectionTimeout before failing. HikariCP configuration.

Observed locally with one connection: the call fails with CannotCreateTransactionException, whose root cause is SQLTransientConnectionException. Both tables remain empty. With maximum and minimum pool sizes changed to two, the same call succeeds and both tables contain one row.

The explanation is a resource dependency. The outer transaction already holds connection A. Suspending its participation does not release that connection back to the pool. The independent inner transaction needs connection B. With capacity one, the request waits for a resource that its own outer scope is retaining.

This is connection starvation, not evidence of a database row-lock deadlock. Check pool active, idle, and waiting counts alongside connection-acquisition timing before blaming a slow query.

One extra connection can allow progress in a simple single-level example, but it is not a universal sizing formula. Nested independent scopes, other borrowers, transaction duration, and database capacity change the requirement. Reduce unnecessary independent transactions and test bounded concurrency as well as pool size.

Independent commits need a business meaning

An audit transaction that survives failure should describe an attempted operation or a failure, not falsely report that the business action succeeded. Decide which statement remains truthful if the outer transaction later rolls back.

Independence also does not guarantee that the inner transaction can see uncommitted outer writes. If the audit insert references an outer-created row, or touches data locked by the outer transaction, it may encounter visibility, constraint, or locking problems. Our two-table fixture deliberately avoids those dependencies; reproduce them separately in the database and isolation level your application uses.

For work that should happen only after a business commit, consider whether the correct design is a record written in the same transaction and processed after commit. Do not claim that changing a propagation setting makes a database write atomic with an email or remote service call.

The interview decision is not “Which annotation saves the audit?” It is “Which facts must commit together, which may survive independently, and what happens if either operation fails?”

Defaults, thread boundaries, and other modes

Official rollback rule: by default, unchecked exceptions and Error trigger rollback; checked exceptions do not. Type-specific rollbackFor rules can change that behavior. Our harness confirms an IOException leaves the inner insert committed under the default, while rollbackFor = IOException.class rolls it back. Rollback rules.

Current Spring also supports changing the global default through @EnableTransactionManagement(rollbackOn = ALL_EXCEPTIONS). Check configuration before announcing what a checked exception will do. Prefer explicit exception types over broad name patterns when configuring a specific rollback rule. Annotation settings.

Official execution-context fact: ordinary imperative transactions are thread-bound and do not automatically propagate into a newly started thread. A proxied transactional method invoked in another thread may establish a separate transaction, but it does not inherit the original one merely because the caller submitted the work. Transactional API.

For follow-ups, know the intent of the remaining modes: MANDATORY requires an existing transaction; NEVER rejects one; SUPPORTS participates if one exists; NOT_SUPPORTED executes without one and suspends existing participation when supported. These modes express boundary requirements, not different isolation levels. Propagation API.

How to defend an answer with evidence

Our local harness passed eleven cases covering self-invocation, proxy entry, caught and uncaught inner failures, savepoints, checked exceptions, and pool exhaustion. The assertions inspect persisted rows after calls complete, as well as the exception type. They do not claim JPA behavior, production throughput, or a concurrency benchmark.

Use the same reasoning sequence on an unfamiliar snippet: mark proxy crossings, draw physical transactions, identify connection ownership, trace the exception, and predict the final rows. Then change one condition and explain which prediction changes.

Useful mutations include moving the catch, switching the inner propagation mode, reducing pool capacity, or adding a foreign key from the audit to uncommitted business data. A test that only verifies that a repository method was called cannot establish that the transaction committed.

Five related questions to practice

Practice questionWhat to check next
Explain Java DI, AOP, and transactionsProxy calls
Explain Spring Dependency Injection and Choose an Injection StyleBean wiring
Assess Spring IoC, AOP, JDBC, SecurityJDBC scope
Design a lazy-initialized connection poolPool limits
Design In-Memory Transactions with Snapshots and SavepointsPartial undo

For each transaction answer, name one persisted outcome and one observation that would prove your explanation wrong.

Sources and Further Reading