Why @Transactional Fails: 13 Hidden Pitfalls and How to Fix Them
Spring’s @Transactional annotation often appears simple, yet it can silently fail due to unnecessary usage, scope issues, proxy limitations, propagation settings, exception handling, and bean management, leading to unexpected non‑rollback behavior; this article categorizes thirteen common pitfalls and demonstrates each with concrete code examples.
Unnecessary
1. Business without transaction
Using @Transactional on methods that only perform queries or simple HTTP calls adds no value and should be removed.
@Transactional
public String testQuery() {
standardBak2Service.getById(1L);
return "testB";
}2. Transaction scope too large
Annotating a whole class or an abstract class makes every method transactional, causing unnecessary performance overhead.
@Transactional
public abstract class BaseService {}
@Service
public class TestMergeService extends BaseService {
private final TestAService testAService;
private final TestBService testBService;
public String testMerge() {
testAService.testA();
testBService.testB();
return "ok";
}
}Not Effective
3. Method visibility
Private methods cannot be proxied, so @Transactional on a private method never takes effect.
@Transactional
private String testMerge() {
testAService.testA();
testBService.testB();
return "ok";
}4. final / static methods
Static methods belong to the class, and final methods cannot be overridden; both cannot be intercepted by Spring AOP.
@Transactional
public static void b() {}
@Transactional
public final void b() {}5. Internal method calls
Calling another method of the same class bypasses the proxy, so the second method's transaction does not start.
@Transactional
public String testMerge() {
a(); // non‑transactional
b(); // transaction ignored
return "ok";
}
public void a() { /* ... */ }
public void b() { throw new RuntimeException("b error"); }Solutions include extracting the called method into a separate @Service bean, self‑injection, or obtaining the current proxy via AopContext.currentProxy().
5.1 Separate service class
@Service
public class TestBService {
@Transactional
public void b() { throw new RuntimeException("b error"); }
}5.2 Self‑injection
@Autowired
private TestMergeService self;
public String testMerge() {
a();
self.b(); // goes through proxy
return "ok";
}5.3 Manual proxy
((TestMergeService) AopContext.currentProxy()).b();6. Bean not managed by Spring
The class must be annotated with @Component, @Service, @Controller, etc., so that Spring can create a proxy.
Not Rolling Back
9. Wrong propagation attribute
Using an inappropriate propagation setting can prevent rollback. The seven propagation types are REQUIRED, MANDATORY, NEVER, REQUIRES_NEW, NESTED, SUPPORTS, NOT_SUPPORTED.
REQUIRED
Default behavior; joins existing transaction or creates a new one.
MANDATORY
Requires an existing transaction; otherwise throws IllegalTransactionStateException.
org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
NEVER
Forces non‑transactional execution; throws exception if a transaction exists.
org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'
REQUIRES_NEW
Always starts a new transaction, suspending any existing one.
NESTED
Creates a nested transaction that rolls back together with the outer transaction.
NOT_SUPPORTED
Executes non‑transactionally, suspending any current transaction.
SUPPORTS
Runs within a transaction if one exists; otherwise non‑transactionally.
10. Swallowing exceptions
Catching exceptions without re‑throwing prevents Spring from detecting the failure, so the transaction will not roll back.
@Transactional
public String testMerge() {
try {
testAService.testA();
testBService.testB();
} catch (Exception e) {
log.error("testMerge error:{}", e);
// re‑throw to trigger rollback
throw new RuntimeException(e);
}
return "ok";
}11. Exceptions that do not trigger rollback
By default Spring rolls back on RuntimeException and Error. Checked exceptions (e.g., SQLException) do not cause rollback unless specified with rollbackFor.
12. Custom exception range
When using custom exceptions, ensure they extend RuntimeException or declare them in rollbackFor.
13. Nested transaction handling
If you want a sub‑transaction to fail without affecting the outer transaction, catch the exception inside the outer method and prevent it from propagating.
Summary
The above points are the result of code reviews and community feedback on the proper use of Spring’s @Transactional annotation. Understanding these pitfalls helps avoid silent failures and reduces the time spent on debugging and manual testing.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
