Mastering Spring Transaction Management: From JDBC to @Transactional

This article explains the fundamentals of transaction handling in Spring Boot, covering both programmatic approaches with PlatformTransactionManager and TransactionTemplate and declarative management using @Transactional, while detailing common pitfalls, propagation and isolation settings, and providing concrete code examples.

Pan Zhi's Tech Notes
Pan Zhi's Tech Notes
Pan Zhi's Tech Notes
Mastering Spring Transaction Management: From JDBC to @Transactional

Transaction Basics

A transaction groups multiple SQL statements so that they either all succeed or all fail, preserving data integrity. For example, a bank transfer must debit one account and credit another atomically; otherwise, inconsistent balances may occur.

Spring Boot Transaction Management

Spring supports two transaction programming models:

Programmatic management using TransactionTemplate or the lower‑level PlatformTransactionManager.

Declarative management using the @Transactional annotation, which relies on AOP interception.

Spring Boot automatically includes the required transaction support libraries; no extra dependencies are needed.

Programmatic Transaction Management

At the JDBC level, manual transaction control is performed by disabling auto‑commit, executing statements, and committing or rolling back the connection:

Class.forName(DRIVER_CLASS);
Connection conn = DriverManager.getConnection(JDBC_URL, USER, PASSWORD);
conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
stmt.executeUpdate("insert into tb_user(id, name) values(1, 'tom')");
stmt.executeUpdate("insert into tb_role(id, name) values(1, 'sys')");
conn.commit();
} catch (SQLException e) {
    conn.rollback();
}
stmt.close();
conn.close();

In Spring, the same logic can be expressed with PlatformTransactionManager:

@Service
public class ApiService {
    @Autowired private RoleMapper roleMapper;
    @Autowired private MenuMapper menuMapper;
    @Autowired private PlatformTransactionManager transactionManager;

    public void insert(Role role, Menu menu) {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            roleMapper.insert(role);
            menuMapper.insert(menu);
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
            LOGGER.error("Commit data exception", e);
        }
    }
}

Running the application prints the concrete transaction‑manager implementation (e.g.

org.springframework.jdbc.datasource.DataSourceTransactionManager

), confirming Spring Boot’s default choice.

Because TransactionTemplate.execute() internally follows the same commit/rollback pattern, the article recommends using TransactionTemplate for most programmatic scenarios due to its concise API.

@Service
public class ApiService {
    @Autowired private RoleMapper roleMapper;
    @Autowired private MenuMapper menuMapper;
    @Autowired private TransactionTemplate transactionTemplate;

    public Integer insert1(Role role, Menu menu) {
        return transactionTemplate.execute(status -> {
            roleMapper.insert(role);
            menuMapper.insert(menu);
            return 1;
        });
    }

    public void insert2(Role role, Menu menu) {
        transactionTemplate.execute(status -> {
            roleMapper.insert(role);
            menuMapper.insert(menu);
            return null;
        });
    }

    public void insert3(Role role, Menu menu) {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                roleMapper.insert(role);
                menuMapper.insert(menu);
            }
        });
    }
}

All three methods achieve the same manual transaction control.

Declarative Transaction Management

Applying @Transactional eliminates boilerplate code. Spring creates a proxy that starts a transaction before the target method executes and commits or rolls back after the method returns.

@Service
public class ApiService {
    @Autowired private RoleMapper roleMapper;
    @Autowired private MenuMapper menuMapper;

    @Transactional
    public void insert(Role role, Menu menu) {
        roleMapper.insert(role);
        menuMapper.insert(menu);
    }
}

The annotation can be placed on classes, methods, or interfaces, but Spring recommends applying it to concrete class methods to avoid proxying issues.

Common Scenarios Where @Transactional May Not Take Effect

Scenario 1: The annotation is on a non‑ public method; Spring only proxies public methods, so the transaction is ignored.

Scenario 2: A method in the same class calls another @Transactional method; the internal call bypasses the proxy, so no transaction is started.

Scenario 3: An exception is caught inside the transactional method; the proxy does not see the exception, so it commits instead of rolling back.

Scenario 4: By default, only unchecked exceptions trigger rollback; checked exceptions will not cause a rollback unless configured.

Scenario 5: Misconfiguration of annotation attributes (e.g., readOnly=true on a write operation) leads to unexpected behavior.

Each scenario is illustrated with a code snippet in the source article.

Transactional Annotation Attributes

public @interface Transactional {
    @AliasFor("transactionManager") String value() default "";
    @AliasFor("value") String transactionManager() default "";
    Propagation propagation() default Propagation.REQUIRED;
    Isolation isolation() default Isolation.DEFAULT;
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
    boolean readOnly() default false;
    Class<? extends Throwable>[] rollbackFor() default {};
    String[] rollbackForClassName() default {};
    Class<? extends Throwable>[] noRollbackFor() default {};
    String[] noRollbackForClassName() default {};
}

Key attributes: transactionManager: selects a specific PlatformTransactionManager bean. propagation: defines how transactions nest (default REQUIRED). isolation: sets the database isolation level. timeout, readOnly, rollbackFor, noRollbackFor: fine‑grained control.

Multiple Transaction Managers

When several data sources exist, define distinct managers and select one via the annotation:

@Configuration
public class TransactionManagerConfigBean {
    @Autowired private DataSource dataSource;

    @Bean(name = "txManager1")
    @Primary
    public PlatformTransactionManager txManager1() {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "txManager2")
    public PlatformTransactionManager txManager2() {
        return new DataSourceTransactionManager(dataSource);
    }
}

@Service
public class ApiService {
    @Autowired private RoleMapper roleMapper;
    @Autowired private MenuMapper menuMapper;

    @Transactional(value = "txManager2")
    public void insert(Role role, Menu menu) throws Exception {
        roleMapper.insert(role);
        menuMapper.insert(menu);
    }
}

Transaction Propagation

Spring supports seven propagation behaviors: REQUIRED: join existing transaction or create a new one (default). 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 any existing one. NOT_SUPPORTED: execute without a transaction, suspending any existing one. NEVER: must not run within a transaction; throws if one exists. NESTED: run within a nested transaction if a current transaction exists; otherwise behaves like REQUIRED.

@Transactional(propagation = Propagation.REQUIRED)
public void someMethod() { ... }

Transaction Isolation Levels

Isolation determines how concurrent transactions interact. Spring maps the following enum values to database isolation levels: DEFAULT: use the database’s default (often REPEATABLE_READ). READ_UNCOMMITTED: allows dirty reads. READ_COMMITTED: prevents dirty reads; data becomes visible only after commit. REPEATABLE_READ: guarantees repeatable reads, preventing non‑repeatable reads. SERIALIZABLE: highest isolation; transactions are fully serialized using read/write locks.

@Transactional(isolation = Isolation.DEFAULT)
public void method() { ... }

Illustrative Failure Cases for @Transactional

Case 1 – Non‑public method

@Service
public class ApiService {
    @Autowired private RoleMapper roleMapper;
    @Autowired private MenuMapper menuMapper;

    @Transactional
    protected void insert(Role role, Menu menu) { // not public
        roleMapper.insert(role);
        menuMapper.insert(menu);
    }
}

The method runs without a transaction; any exception will not trigger rollback.

Case 2 – Internal method call

@Service
public class ApiService {
    @Autowired private RoleMapper roleMapper;
    @Autowired private MenuMapper menuMapper;

    public void save(Role role, Menu menu) {
        insert(role, menu); // internal call bypasses proxy
    }

    @Transactional
    public void insert(Role role, Menu menu) {
        roleMapper.insert(role);
        menuMapper.insert(menu);
    }
}

Calling save() does not start a transaction because the proxy is not involved.

Case 3 – Swallowed exception

@Service
public class ApiService {
    @Transactional
    public void insert(Role role, Menu menu) {
        try {
            roleMapper.insert(role);
            menuMapper.insert(menu);
        } catch (Exception e) {
            e.printStackTrace(); // exception consumed, transaction commits
        }
    }
}

Because the exception is caught, the proxy does not see it and commits the transaction.

Case 4 – Checked exception without rollback configuration

@Service
public class ApiService {
    @Transactional
    public void insert(Role role, Menu menu) throws Exception {
        try {
            roleMapper.insert(role);
            menuMapper.insert(menu);
        } catch (Exception e) {
            throw new Exception("save error"); // checked exception
        }
    }
}

Only unchecked exceptions trigger rollback by default; the checked Exception does not cause a rollback. To roll back on checked exceptions, configure rollbackFor = Exception.class on the annotation.

Case 5 – Misused readOnly flag

@Service
public class ApiService {
    @Transactional(readOnly = true)
    public void insert(Role role, Menu menu) {
        roleService.insert(role);
        menuService.insert(menu);
    }
}

Setting readOnly=true marks the transaction as read‑only, causing write operations to fail.

Transactional Annotation Source Overview

The annotation definition (shown above) includes the attributes discussed. The most frequently tuned attributes are transactionManager, propagation, and isolation; other attributes are self‑explanatory.

Configuring Transaction Manager Beans

@Configuration
public class TransactionManagerConfigBean {
    @Autowired private DataSource dataSource;

    @Bean(name = "txManager1")
    @Primary
    public PlatformTransactionManager txManager1() {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "txManager2")
    public PlatformTransactionManager txManager2() {
        return new DataSourceTransactionManager(dataSource);
    }
}

Specify the desired manager in @Transactional(value = "txManager2").

Propagation Example with Mixed Programmatic and Declarative Calls

@Service
public class ApiService {
    @Autowired private RoleService roleService;
    @Autowired private MenuService menuService;
    @Autowired private PlatformTransactionManager transactionManager;

    public void save(Role role, Menu menu) {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            roleService.insert(role); // @Transactional(REQUIRED)
            menuService.insert(menu); // @Transactional(REQUIRED)
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
        }
    }
}

@Service
public class RoleService {
    @Autowired private RoleMapper roleMapper;
    @Transactional
    public void insert(Role role) { roleMapper.insert(role); }
}

@Service
public class MenuService {
    @Autowired private MenuMapper menuMapper;
    @Autowired private TransactionTemplate transactionTemplate;
    @Transactional
    public void insert(Menu menu) {
        transactionTemplate.execute(status -> { menuMapper.insert(menu); return null; });
    }
}

When save() throws an exception, all inserts are rolled back because the outer programmatic transaction encompasses the inner declarative ones.

Summary

Programmatic transaction management offers fine‑grained control and is suitable for long‑running or complex operations. Declarative @Transactional provides concise syntax for typical use cases but requires careful placement to avoid the five common pitfalls that can cause transaction loss. Proper configuration of propagation, isolation, timeout, and rollback rules ensures reliable transactional behavior.

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.

Springspring-bootJDBCTransaction ManagementProgrammatic TransactionDeclarative TransactionTransactional Annotation
Pan Zhi's Tech Notes
Written by

Pan Zhi's Tech Notes

Sharing frontline internet R&D technology, dedicated to premium original content.

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.