Mastering Spring Boot Transactions: From Basics to Advanced Settings

This article explains what database transactions are, demonstrates how to use Spring Boot’s @Transactional annotation with unit tests, and dives into transaction isolation levels and propagation behaviors, providing code examples and guidance for effective transaction management in Java backend applications.

Programmer DD
Programmer DD
Programmer DD
Mastering Spring Boot Transactions: From Basics to Advanced Settings

What is a Transaction?

When developing enterprise applications, a business operation often involves multiple database read/write steps. If any step fails, the previous successful operations become unreliable. Two ways to handle this are:

Record the failure point and resume after fixing the issue.

Rollback the entire operation so the system returns to its original state before retrying.

Transaction implements the second approach, ensuring all steps succeed or all fail. For example, creating an order and deducting inventory must be wrapped in a transaction to avoid overselling.

Quick Start

In Spring Boot, adding spring-boot-starter-jdbc or spring-boot-starter-data-jpa automatically configures a transaction manager, so you can use @Transactional without extra configuration.

Using the previous Spring Data JPA example, we create a test that inserts 10 users. The test deliberately triggers an exception by setting a maximum age of 50 on the User entity.

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {

    @Autowired
    private UserRepository userRepository;

    @Test
    public void test() throws Exception {
        // Create 10 records
        userRepository.save(new User("AAA", 10));
        userRepository.save(new User("BBB", 20));
        userRepository.save(new User("CCC", 30));
        userRepository.save(new User("DDD", 40));
        userRepository.save(new User("EEE", 50));
        userRepository.save(new User("FFF", 60));
        userRepository.save(new User("GGG", 70));
        userRepository.save(new User("HHH", 80));
        userRepository.save(new User("III", 90));
        userRepository.save(new User("JJJ", 100));
        // ... other verification steps
    }
}

Running the test without @Transactional shows that after the exception only the first five records are persisted.

The User entity is defined as:

@Entity
@Data
@NoArgsConstructor
public class User {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @Max(50)
    private Integer age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

Executing the test produces a ConstraintViolationException and rolls back the transaction, leaving no records in the database.

2020-07-09 11:55:29.581 ERROR ... Validation failed for classes [com.didispace.chapter310.User] during persist time ... ConstraintViolationImpl{interpolatedMessage='最大不能超过50', propertyPath=age, ...}

Adding @Transactional to the test method causes the transaction to roll back automatically.

@Test
@Transactional
public void test() throws Exception {
    // ... test content
}

In real applications, @Transactional is typically placed on service‑layer methods.

public interface UserService {
    @Transactional
    User update(String name, String password);
}

Transaction Details

The default configuration works for basic needs, but complex projects may require specifying a particular transaction manager, isolation level, or propagation behavior.

Isolation Levels

Isolation determines how concurrent transactions are isolated from each other, affecting dirty reads, non‑repeatable reads, and phantom reads.

public enum Isolation {
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);
}
DEFAULT

: uses the database’s default, usually READ_COMMITTED. READ_UNCOMMITTED: allows reading uncommitted changes; prone to dirty reads. READ_COMMITTED: prevents dirty reads; recommended for most cases. REPEATABLE_READ: prevents dirty and non‑repeatable reads. SERIALIZABLE: fully isolates transactions but impacts performance.

Set isolation with @Transactional(isolation = Isolation.DEFAULT).

Propagation Behaviors

Propagation defines how a method participates in existing transactions.

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);
}
REQUIRED

: join existing or create new. SUPPORTS: join if exists, otherwise non‑transactional. MANDATORY: must have an existing transaction. REQUIRES_NEW: always start a new transaction, suspending any existing. NOT_SUPPORTED: execute non‑transactionally, suspending any existing. NEVER: must not have a transaction. NESTED: execute within a nested transaction if one exists.

Set propagation with @Transactional(propagation = Propagation.REQUIRED).

Code Repository

All examples are available in the chapter3-10 directory of the SpringBoot‑Learning project on GitHub and Gitee.

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.

JavadatabaseSpring Boottransaction-managementpropagationIsolation Level
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

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.