Mastering Spring Transactions: From Basics to Advanced Propagation Types

This article explains the origins and purpose of Spring transactions, demonstrates how to quickly set them up with code examples, explores key features and common pitfalls, and provides an in‑depth comparison of the REQUIRED, REQUIRES_NEW, and NESTED propagation types for robust backend development.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Mastering Spring Transactions: From Basics to Advanced Propagation Types

Spring transactions are essential for handling complex, consistent business operations. Understanding their background helps clarify why they exist and how they differ from plain database transactions.

Background

A transaction is a unit of work that must be completed entirely or not at all, characterized by the ACID properties:

Atomicity : all operations succeed or none do.

Consistency : database integrity is preserved before and after the transaction.

Isolation : concurrent transactions do not interfere with each other.

Durability : once committed, changes survive system failures.

Spring implements these concepts using AOP to start, commit, or roll back transactions at the appropriate points in the business layer.

Why Use Spring Transactions?

Consider a bank transfer: deducting money from one account and adding it to another must happen atomically. Spring transactions ensure both operations succeed together or are both rolled back, preventing data inconsistency.

If you are familiar with MySQL transactions, you know they can be controlled manually. Spring abstracts this by applying transaction boundaries automatically via annotations or programmatic APIs.

Usage Guide

Spring supports two usage styles: declarative (annotation‑based) and programmatic. Declarative is more common.

Project Setup

Create two simple tables, tablea and tableb, each with id and name columns.

CREATE TABLE `tablea` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1;
CREATE TABLE `tableb` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1;

Add Spring Boot, MyBatis, and MySQL dependencies to the pom.xml:

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>2.1.0</version>
</dependency>

Implement the controller, service, mapper, and entity classes as shown below.

@SpringBootApplication
@RestController
@RequestMapping("/api")
public class SpringTransactionController {

    @Autowired
    private TransactionServiceA transactionServiceA;

    @RequestMapping("/spring-transaction")
    public String testTransaction() {
        transactionServiceA.methodA();
        return "SUCCESS";
    }
}
public interface TableService {
    void insertTableA(TableEntity tableEntity);
    void insertTableB(TableEntity tableEntity);
}
@Service
public class TransactionServiceA {

    @Autowired
    private TableService tableService;

    @Autowired
    private TransactionServiceB transactionServiceB;

    public void methodA(){
        System.out.println("methodA");
        tableService.insertTableA(new TableEntity());
        transactionServiceB.methodB();
    }
}
@Service
public class TransactionServiceB {

    @Autowired
    private TableService tableService;

    public void methodB(){
        System.out.println("methodB");
        tableService.insertTableB(new TableEntity());
    }
}
@Mapper
public interface TableMapper {
    @Insert("INSERT INTO tablea(id, name) " +
            "VALUES(#{id}, #{name})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    void insertTableA(TableEntity tableEntity);

    @Insert("INSERT INTO tableb(id, name) " +
            "VALUES(#{id}, #{name})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    void insertTableB(TableEntity tableEntity);
}
@Data
public class TableEntity {
    private static final long serialVersionUID = 1L;
    private Long id;
    private String name;
    public TableEntity() {}
    public TableEntity(String name) { this.name = name; }
}

Configure the datasource in application.yml (or application.properties).

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
  type-aliases-package: tech.shuyi.javacodechip.spring_transaction.model
  configuration:
    map-underscore-to-camel-case: true

Run the Spring Boot application and access http://localhost:8080/api/spring-transaction. If everything is set up correctly, both tablea and tableb will contain a new row.

Transaction Propagation Types

Propagation defines how transactions interact when one transactional method calls another. Spring defines seven types; the most commonly used are REQUIRED, REQUIRES_NEW, and NESTED.

REQUIRED

When a transaction already exists, the called method joins it; otherwise a new transaction is created. All participating methods share a single transaction, so any exception causes a rollback of the whole unit.

public void methodA(){
    System.out.println("methodA");
    tableService.insertTableA(new TableEntity());
    transactionServiceB.methodB();
}

@Transactional
public void methodB(){
    System.out.println("methodB");
    tableService.insertTableB(new TableEntity());
    throw new RuntimeException();
}

Result: tablea is inserted, tableb is not, because the transaction rolls back.

REQUIRES_NEW

This propagation always starts a new, independent transaction for the called method, regardless of any existing transaction. Parent and child transactions do not affect each other unless the parent propagates the child's exception.

@Transactional
public void methodA(){
    tableService.insertTableA(new TableEntity());
    transactionServiceB.methodB();
    throw new RuntimeException();
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
    tableService.insertTableB(new TableEntity());
}

Result: tablea rolls back, but tableb remains committed.

If the child throws an exception and the parent catches it, the parent transaction can still commit:

@Transactional
public void methodA(){
    tableService.insertTableA(new TableEntity());
    try {
        transactionServiceB.methodB();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
    tableService.insertTableB(new TableEntity());
    throw new RuntimeException();
}

Result: tablea is inserted, tableb is not.

NESTED

NESTED creates a save‑point within the existing transaction. If the nested transaction rolls back, the outer transaction can decide whether to continue based on exception handling.

@Transactional
public void methodA(){
    tableService.insertTableA(new TableEntity());
    transactionServiceB.methodB();
    throw new RuntimeException();
}

@Transactional(propagation = Propagation.NESTED)
public void methodB(){
    tableService.insertTableB(new TableEntity());
}

Result: both tables roll back because the outer transaction fails.

@Transactional
public void methodA(){
    tableService.insertTableA(new TableEntity());
    transactionServiceB.methodB();
}

@Transactional(propagation = Propagation.NESTED)
public void methodB(){
    tableService.insertTableB(new TableEntity());
    throw new RuntimeException();
}

Result: both tables roll back when the nested transaction throws an exception and the parent does not catch it.

@Transactional
public void methodA(){
    tableService.insertTableA(new TableEntity());
    try {
        transactionServiceB.methodB();
    } catch (Exception e) {
        // handle
    }
}

@Transactional(propagation = Propagation.NESTED)
public void methodB(){
    tableService.insertTableB(new TableEntity());
    throw new RuntimeException();
}

Result: tablea is committed, tableb is rolled back because the exception is caught.

Summary of Propagation Types

REQUIRED : Joins existing transaction or creates a new one; all participants share the same transaction, so any failure rolls back the whole unit.

REQUIRES_NEW : Always starts a new, independent transaction; parent and child transactions are isolated, but the parent must handle child exceptions to avoid unintended rollbacks.

NESTED : Executes within a save‑point of the existing transaction; the outer transaction can decide to continue if the nested one fails and the exception is caught.

How to Use Spring Transactions Effectively

Analyze the business scenario and decide which propagation type achieves the desired transactional behavior.

Annotate service‑layer methods with @Transactional and set the appropriate propagation attribute.

Remember that Spring transactions only work on public methods called from outside the bean, that the bean must be managed by Spring, and that the underlying database must support transactions.

When Transactions May Fail

If a method without @Transactional calls a transactional method within the same class, the transaction will not be applied because Spring AOP proxies only intercept external calls.

Also, @Transactional only works on public methods, and the class must be a Spring‑managed component.

Easter Egg

Spring rolls back only for unchecked exceptions (RuntimeException or Error). Checked exceptions do not trigger a rollback unless explicitly configured, so both tables will be inserted when a checked Exception is thrown.

@Transactional
public void methodA() throws Exception {
    tableService.insertTableA(new TableEntity());
    transactionServiceB.methodB();
}

@Transactional
public void methodB() throws Exception {
    tableService.insertTableB(new TableEntity());
    throw new Exception(); // checked exception, no rollback
}

References

SegmentFault: "从头到尾说一次 Spring 事务管理(器)"

ZhiHu: "Spring 事务原理一探"

JavaGuide: "Spring 事务详解"

博客园: "事务之六:spring 嵌套事务"

博客园: "NESTED 区别!spring 事务传播行为详解"

Michael 翔: "Spring Boot 实战 —— MyBatis(注解版)使用方法"

云扬四海: "记一次事务的坑 Transaction rolled back because it has been marked as rollback-only"

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavatransactionaopspringMyBatispropagation
Su San Talks Tech
Written by

Su San Talks Tech

Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.