Why Your Spring @Transactional Might Fail: 7 Common Pitfalls and How to Fix Them

This article explains why Spring transactions can become ineffective or fail to roll back, covering issues such as wrong method visibility, final methods, internal calls, missing Spring bean registration, multithreading, unsupported table engines, misconfigured propagation, exception handling, and offers practical solutions for each case.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Why Your Spring @Transactional Might Fail: 7 Common Pitfalls and How to Fix Them

Introduction

For Java developers, Spring transactions are familiar. When a request needs to write multiple tables atomically, we use @Transactional. However, misuse can cause the transaction to fail.

1. Transaction Not Effective

1.1 Access Modifier Issue

Spring requires the proxied method to be public; if it is private, protected or package‑private, the transaction is ignored.

@Service
public class UserService {
    @Transactional
    private void add(UserModel userModel) {
        saveData(userModel);
        updateData(userModel);
    }
}

The AbstractFallbackTransactionAttributeSource class checks the method modifiers and returns null for non‑public methods, so no transaction attribute is applied.

1.2 Method Declared final

Final methods cannot be overridden by the proxy generated by Spring AOP, therefore the transaction logic is never added.

@Service
public class UserService {
    @Transactional
    public final void add(UserModel userModel) {
        saveData(userModel);
        updateData(userModel);
    }
}
Note: static methods also cannot be proxied.

1.3 Internal Method Call

Calling a transactional method from another method in the same class uses the this reference, bypassing the proxy, so the transaction does not take effect.

Solutions:

3.1 Add a New Service Method

@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
    public void save(User user) {
        queryData1();
        queryData2();
        serviceB.doSave(user);
    }
}

@Service
public class ServiceB {
    @Transactional(rollbackFor = Exception.class)
    public void doSave(User user) {
        addData1();
        updateData2();
    }
}

3.2 Inject Self

@Service
public class ServiceA {
    @Autowired
    private ServiceA self;
    public void save(User user) {
        queryData1();
        queryData2();
        self.doSave(user);
    }
    @Transactional(rollbackFor = Exception.class)
    public void doSave(User user) {
        addData1();
        updateData2();
    }
}

3.3 Use AopContext.currentProxy()

@Service
public class ServiceA {
    public void save(User user) {
        queryData1();
        queryData2();
        ((ServiceA) AopContext.currentProxy()).doSave(user);
    }
    @Transactional(rollbackFor = Exception.class)
    public void doSave(User user) {
        addData1();
        updateData2();
    }
}

1.4 Not Managed by Spring

Only beans created by Spring (annotated with @Service, @Component, @Repository, etc.) are proxied. If a class lacks such an annotation, @Transactional has no effect.

//@Service
public class UserService {
    @Transactional
    public void add(UserModel userModel) {
        saveData(userModel);
        updateData(userModel);
    }
}

1.5 Multithreaded Calls

Transactions are bound to the current thread via a ThreadLocal. Invoking transactional code in another thread creates a separate connection, so rollback does not propagate.

@Slf4j
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;
    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        new Thread(() -> {
            roleService.doOtherThing();
        }).start();
    }
}

@Service
public class RoleService {
    @Transactional
    public void doOtherThing() {
        System.out.println("save role data");
    }
}

The transaction resources are stored in a ThreadLocal<Map<Object, Object>>, so different threads use different connections.

1.6 Table Does Not Support Transaction

MyISAM engine (used before MySQL 5) does not support transactions. Using such tables prevents transaction commit/rollback.

CREATE TABLE `category` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
When a table’s engine does not support transactions, the transaction will never be effective.

1.7 Transaction Not Enabled

In Spring Boot, DataSourceTransactionManagerAutoConfiguration enables transaction management automatically. In a traditional Spring project you must configure a DataSourceTransactionManager bean and tx:advice in XML.

<!-- Configure transaction manager -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<tx:advice id="advice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>
<!-- Apply advice to beans -->
<aop:config>
    <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/>
    <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config>

If the pointcut expression is wrong, some beans will not be proxied and their transactions will not work.

2. Transaction Not Rolling Back

2.1 Wrong Propagation Setting

Using a propagation type that does not create a transaction (e.g., Propagation.NEVER) will prevent rollback.

@Service
public class UserService {
    @Transactional(propagation = Propagation.NEVER)
    public void add(UserModel userModel) {
        saveData(userModel);
        updateData(userModel);
    }
}

2.2 Swallowing Exceptions

Catching exceptions without re‑throwing them makes Spring think the method completed successfully, so no rollback occurs.

@Slf4j
@Service
public class UserService {
    @Transactional
    public void add(UserModel userModel) {
        try {
            saveData(userModel);
            updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

2.3 Throwing the Wrong Exception Type

Spring rolls back only on RuntimeException or Error by default. Throwing a checked Exception does not trigger rollback.

@Slf4j
@Service
public class UserService {
    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
            saveData(userModel);
            updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }
}

2.4 Misconfigured rollbackFor

If rollbackFor is set to a custom exception that is never thrown, the transaction will not roll back when other exceptions occur.

@Slf4j
@Service
public class UserService {
    @Transactional(rollbackFor = BusinessException.class)
    public void add(UserModel userModel) throws Exception {
        saveData(userModel);
        updateData(userModel);
    }
}

SQL exceptions such as DuplicateKeyException will not trigger rollback in this configuration.

2.5 Nested Transaction Rollback Propagation

When a nested transaction throws an exception that propagates to the outer transaction, the whole transaction is rolled back unless the inner exception is caught and not re‑thrown.

public class UserService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;
    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        roleService.doOtherThing();
    }
}

@Service
public class RoleService {
    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
        System.out.println("save role data");
    }
}

Solution: wrap the inner call in a try/catch block and do not re‑throw the exception.

@Transactional
public void add(UserModel userModel) throws Exception {
    userMapper.insertUser(userModel);
    try {
        roleService.doOtherThing();
    } catch (Exception e) {
        log.error(e.getMessage(), e);
    }
}

3. Other Issues

3.1 Large Transaction Problem

Annotating an entire method can unintentionally include many read‑only queries, leading to long‑running transactions and performance problems.

3.2 Programmatic Transaction

Using TransactionTemplate gives explicit control over transaction boundaries and avoids AOP proxy pitfalls.

@Autowired
private TransactionTemplate transactionTemplate;

public void save(final User user) {
    queryData1();
    queryData2();
    transactionTemplate.execute(status -> {
        addData1();
        updateData2();
        return Boolean.TRUE;
    });
}
Prefer programmatic transactions for complex scenarios, but @Transactional remains convenient for simple use cases.
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.

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