Transaction propagation determines whether a call joins an existing transaction or runs with another transaction boundary.
Spring transaction propagation: joined rollback and independent commit
The downloadable Spring source kit pins Java 21 and Spring Boot 4.0.8 with its managed dependencies. Run mvn test to check the named fixture.
Joined work shares rollback fate
TransactionContracts starts a REQUIRED outer transaction and inserts a pending journal row. An inner REQUIRED template marks its status rollback-only. The outer callback returns normally, but its attempted commit raises UnexpectedRollbackException and the table stays empty. Catching an inner application exception would not necessarily make the transaction committable.
The fixture uses templates so the propagation boundary is visible without proxy routing. An annotation-based service needs the call to cross the appropriate proxy; self-invocation can bypass the intended interception. Read Transaction proxies before translating this template into service annotations.
Independent work has an independent consequence
The second path inserts a pending row in the outer transaction, suspends it for REQUIRES_NEW, and commits a different audit row. The outer then rolls back. Only the audit row remains. That behavior is appropriate only if an audit entry is intended to survive a failed import.
An independent transaction can need another connection while the outer one remains held. The fixture uses separate H2 connections and unrelated primary keys. A bounded pool or shared locked row can change failure behavior; nesting REQUIRES_NEW under load needs a pool-capacity and lock analysis. Connection-pool boundaries provide that context.
Checked source
package in.aitrove.contracts;
import java.util.UUID;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.*;
import org.springframework.transaction.*;
import org.springframework.transaction.support.TransactionTemplate;
public class TransactionContracts {
public final JdbcTemplate jdbc;
public final PlatformTransactionManager manager;
public TransactionContracts() {
var data=new DriverManagerDataSource("jdbc:h2:mem:propagation_"+UUID.randomUUID()+";DB_CLOSE_DELAY=-1","sa","");
jdbc=new JdbcTemplate(data); manager=new DataSourceTransactionManager(data);
jdbc.execute("create table journal(id int primary key, message varchar(30))");
}
public TransactionTemplate transaction(int propagation) {
var tx=new TransactionTemplate(manager);tx.setPropagationBehavior(propagation);return tx;
}
public void independentAuditSurvivesRollback() {
transaction(TransactionDefinition.PROPAGATION_REQUIRED).executeWithoutResult(outer -> {
jdbc.update("insert into journal values(1,'pending')");
transaction(TransactionDefinition.PROPAGATION_REQUIRES_NEW).executeWithoutResult(inner ->
jdbc.update("insert into journal values(2,'audit')"));
outer.setRollbackOnly();
});
}
public void joinedRollbackRejectsCommit() {
transaction(TransactionDefinition.PROPAGATION_REQUIRED).executeWithoutResult(outer -> {
jdbc.update("insert into journal values(3,'pending')");
transaction(TransactionDefinition.PROPAGATION_REQUIRED).executeWithoutResult(inner -> inner.setRollbackOnly());
});
}
public int count() { return jdbc.queryForObject("select count(*) from journal",Integer.class); }
}Test the boundary
FrameworkBoundaryTest.requiresNewAuditSurvivesOuterRollback and requiredInnerRollbackRejectsOuterCommit checks this contract in the source kit. Excerpts belong to the named classes; use the downloadable files for imports, configuration and assertions.
Costs and boundaries
The test verifies two propagation paths, not every database schedule. Suspending a transaction retains its resources until resumed. An inner independent transaction adds connection demand and cannot be assumed to avoid deadlocks merely because it has a separate name.
Common Mistakes
- Do not assume an outer commit succeeds after joined work marked rollback-only.
- Choose intentionally whether an audit must survive rollback.
- Budget extra connections for independent nested transactions.
Read next
Spring TransactionTemplate: roll back a failed multi-row change, Spring transactional methods: call paths and rollback assumptions, Spring transaction events: run a listener after commit without claiming durability, Java connection pools: borrowed handles, rollback and admission timeout.
Continue with checked Spring boundaries
Continue with Spring nested transactions: roll back one step to a JDBC savepoint, Spring NESTED versus REQUIRES_NEW: savepoint or independent commit.
Continue with checked relay behavior
Continue with Spring outbox transaction boundaries: where atomicity ends.
