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.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Why @Transactional Fails: 13 Hidden Pitfalls and How to Fix Them

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.

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.

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