Backend Development 15 min read

Understanding Spring Transaction Management: Concepts, Propagation, Isolation, and Practical Examples

This article explains Spring's transaction management, covering ACID principles, transaction propagation types, isolation levels, timeout settings, read‑only flags, rollback behavior, and the effect of asynchronous execution, supplemented with practical code examples and common pitfalls.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Understanding Spring Transaction Management: Concepts, Propagation, Isolation, and Practical Examples

Questions

What is a transaction?

What functions does Spring transaction provide?

Will Spring transaction isolation override the database isolation level?

What is the difference between NESTED and REQUIRED propagation?

Does a Spring transaction work in asynchronous threads?

Introduction

In everyday development a transaction means a group of SQL statements that must either all succeed or all fail, guaranteeing data consistency (ACID).

Atomicity : the smallest indivisible unit of work.

Consistency : data remains consistent before and after the transaction.

Isolation : concurrent transactions do not interfere with each other.

Durability : once committed, changes survive failures.

Spring Transaction

Spring implements transaction management using AOP. It can be used programmatically or declaratively; the article focuses on the declarative approach with the @Transactional annotation.

Programmatic : flexible but hard to maintain.

Declarative : moves transaction code out of business methods; simply annotate with @Transactional .

Usage

Most scenarios use the @Transactional annotation. The annotation definition is shown below:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";
    @AliasFor("value")
    String transactionManager() default "";
    Propagation propagation() default Propagation.REQUIRED;
    Isolation isolation() default Isolation.DEFAULT;
    boolean readOnly() default false;
    int timeout() default -1;
    Class
[] rollbackFor() default {};
    String[] rollbackForClassName() default {};
    Class
[] noRollbackFor() default {};
    String[] noRollbackForClassName() default {};
}

The annotation can be placed on a class (affecting all its methods) or on a single method.

Preparation

SQL to create the required tables:

-- user table
create table temp_user (
    id bigint primary key,
    name varchar(255)
);

-- user action table
create table temp_user_action (
    id bigint primary key,
    user_id bigint,
    action smallint
);

Java repository fields and a simple service method:

@Resource
private TempUserRepo tempUserRepo;

@Resource
private TempUserActionRepo tempUserActionRepo;

@Transactional
public void test() {
    tempUserRepo.saveUser(new TempUser(1, "1"));
    tempUserActionRepo.saveUserAction(new TempUserAction(1, 1, 1));
}

Propagation

Spring defines seven propagation behaviors:

REQUIRED : join existing transaction or create a new one.

SUPPORTS : join if a transaction exists, otherwise execute non‑transactionally.

MANDATORY : must run within a transaction, otherwise throw an exception.

REQUIRES_NEW : always start a new transaction, suspending the current one.

NOT_SUPPORTED : suspend any existing transaction.

NEVER : must not run within a transaction.

NESTED : run a nested transaction; if the outer transaction rolls back, the nested one rolls back as well.

The default is REQUIRED . An example demonstrates the difference between NESTED and REQUIRED when an exception occurs inside the nested call.

@Transactional
public void test() {
    tempUserRepo.saveUser(new TempUser(1, "spring事务"));
    tempUserActionRepo.saveUserAction(new TempUserAction(1, 1, 1));
}

@Transactional(propagation = Propagation.NESTED)
public void saveUser(TempUser tempUser) {
    tempUserMapper.insert(tempUser);
}

@Transactional(propagation = Propagation.NESTED)
public void saveUserAction(TempUserAction tempUserAction) {
    tempUserActionMapper.insert(tempUserAction);
    try {
        int i = 1 / 0;
    } catch (Exception e) {
        throw new RuntimeException();
    }
}

Result: the user record is committed, the user‑action record is rolled back because the nested transaction created a savepoint.

Isolation

Setting isolation level in Spring translates to executing SET TRANSACTION ISOLATION LEVEL … before the transaction starts. Example using READ_UNCOMMITTED :

@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void test() {
    TempUser tempUser = tempUserRepo.getById(1);
    log.info("User: {}", tempUser);
}

The database still reports its global level (e.g., REPEATABLE_READ), but the current transaction runs with the overridden level.

Timeout

Timeout is specified in seconds; if the method exceeds the limit, a TransactionTimedOutException is thrown.

@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 1)
public void test() {
    try { Thread.sleep(3000L); } catch (Exception e) {}
    tempUserRepo.saveUser(new TempUser(2, "spring事务"));
}

Read‑Only

When readOnly = true , any write operation will cause a Connection is read‑only exception.

Rollback / No‑Rollback

By default, any RuntimeException triggers a rollback. The noRollbackFor attribute can exclude specific exceptions (usually non‑database exceptions).

@Transactional(noRollbackFor = {RuntimeException.class})
public void test() {
    tempUserRepo.saveUser(new TempUser(2, "spring事务"));
    tempUserActionRepo.saveUserAction(new TempUserAction(2, 2, 2));
    try { int i = 1/0; } catch (Exception e) { throw new RuntimeException(); }
}

Asynchronous Threads

Annotating a method called from a new thread with @Transactional creates a separate transaction that works independently of the main thread.

@Transactional
public void test() {
    tempUserActionRepo.saveUserAction(new TempUserAction(3, 1, 1));
    new Thread(() -> asyncService.asyncMethod()).start();
    tempUserActionRepo.saveUserAction(new TempUserAction(4, 1, 1));
}

@Transactional
public void asyncMethod() {
    tempUserRepo.saveUser(new TempUser(1, "spring事务"));
    try { int i = 10/0; } catch (Exception e) { throw new RuntimeException(); }
    tempUserActionRepo.saveUserAction(new TempUserAction(1, 1, 1));
}

Both actions in the async method are rolled back, while the main thread actions are committed.

Precautions

No transaction manager configured.

Service not managed by Spring.

Method is final , static , or non‑public.

Internal method calls within the same class.

Database does not support transactions.

Exception swallowed without rethrowing.

Conclusion

Spring transactions provide ACID guarantees and extend them with propagation, isolation, timeout, read‑only, and fine‑grained rollback control. Spring can override the database isolation level with SET TRANSACTION ISOLATION LEVEL … , and NESTED uses savepoints to roll back only part of the work, while REQUIRED rolls back the whole transaction. Transactions also work in asynchronous threads when the method is annotated.

backendJavatransactionAOPDatabaseSpringPropagation
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

login 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.