Backend Development 20 min read

Why @Transactional Often Fails: 13 Real-World Pitfalls and How to Fix Them

An in‑depth guide reveals thirteen common pitfalls that cause Spring’s @Transactional annotation to malfunction—ranging from unnecessary usage and proxy limitations to propagation misconfigurations and exception handling—plus practical demos and solutions to ensure reliable transaction rollbacks in Java backend development.

macrozheng
macrozheng
macrozheng
Why @Transactional Often Fails: 13 Real-World Pitfalls and How to Fix Them

When taking on a new project, the misuse of the

@Transactional

annotation can lead to unexpected failures and non‑rollback situations. This article categorizes the problems into three main groups—unnecessary, ineffective, and non‑rollback—and demonstrates each scenario with concrete code examples.

Unnecessary

1. Business without transaction

Applying

@Transactional

to methods that only perform queries or HTTP calls adds no value and violates coding standards; it should be removed.

<code>@Transactional
public String testQuery() {
    standardBak2Service.getById(1L);
    return "testB";
}
</code>

2. Transaction scope too large

Annotating a class or abstract class with

@Transactional

makes every method inherit transaction management, causing unnecessary performance overhead. Use the annotation only on methods that truly need it.

<code>@Transactional
public abstract class BaseService {
}

@Slf4j
@Service
public class TestMergeService extends BaseService {
    private final TestAService testAService;
    @Transactional
    public String testMerge() {
        testAService.testA();
        return "ok";
    }
}
</code>

Not Effective

3. Method visibility issues

Private methods cannot be proxied by Spring AOP, so

@Transactional

on a private method never takes effect.

<code>@Transactional
private String testMerge() {
    testAService.testA();
    testBService.testB();
    return "ok";
}
</code>

When a public method calls a private method, the transaction still works because the proxy intercepts the public method.

<code>@Transactional
public String testMerge() throws Exception {
    ccc();
    return "ok";
}

private void ccc() {
    testAService.testA();
    testBService.testB();
}
</code>

4. Final or static methods

Methods declared

final

or

static

cannot be intercepted, so the annotation is ignored.

<code>@Transactional
public static void b() {}

@Transactional
public final void b() {}
</code>

5. Internal method calls

Calling another method of the same class bypasses the proxy, causing the transaction to be ineffective. Solutions include extracting the called method to a separate service, self‑injection, or manually obtaining the proxy via

AopContext.currentProxy()

.

<code>// Separate service
@Service
public class TestBService {
    @Transactional
    public void b() {
        // ...
        throw new RuntimeException("b error");
    }
}

// Self‑injection
@Service
public class TestMergeService {
    @Autowired
    private TestMergeService self;
    @Transactional
    public void b() { /* ... */ }
    public String testMerge() {
        a();
        self.b();
        return "ok";
    }
}

// Manual proxy
public String testMerge() {
    a();
    ((TestMergeService) AopContext.currentProxy()).b();
    return "ok";
}
</code>

6. Bean not managed by Spring

The transaction mechanism works only for beans instantiated by Spring (e.g., annotated with

@Component

,

@Service

, or

@Controller

).

<code>@Service
public class TestBService {
    @Transactional
    public String testB() {
        // ...
        return "testB";
    }
}
</code>

7. Asynchronous thread calls

Transactions are thread‑local; executing transactional code in a new thread prevents rollback propagation.

<code>@Transactional
public String testMerge() {
    testAService.testA();
    new Thread(() -> {
        try {
            testBService.testB();
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }).start();
    return "ok";
}
</code>

Not Rollback

9. Wrong propagation attribute

The

propagation

setting controls how transactions are nested. Misusing it can prevent rollbacks. The seven propagation types are:

REQUIRED

: default, joins existing or creates new.

MANDATORY

: requires an existing transaction.

NEVER

: must not run within a transaction.

REQUIRES_NEW

: always starts a new transaction.

NESTED

: creates a nested transaction.

SUPPORTS

: runs transactionally only if a transaction exists.

NOT_SUPPORTED

: forces non‑transactional execution.

Examples for each propagation type are provided in the original article.

10. Swallowing exceptions

If a method catches an exception without re‑throwing it, Spring cannot trigger a rollback. Re‑throw a

RuntimeException

or configure

rollbackFor

accordingly.

<code>@Transactional
public String testMerge() {
    try {
        testAService.testA();
        testBService.testB();
    } catch (Exception e) {
        log.error("testMerge error:{}", e);
        throw new RuntimeException(e);
    }
    return "ok";
}
</code>

11. Exceptions not causing rollback

Spring rolls back on unchecked exceptions (

RuntimeException

and

Error

) by default. Checked exceptions (e.g.,

SQLException

) do not trigger rollback unless specified with

rollbackFor

.

<code>@Transactional(rollbackFor = Exception.class)
public String testMerge() throws Exception {
    // ...
    throw new Exception(e);
}
</code>

12. Custom exception scope

When using custom exceptions, ensure they extend

RuntimeException

or declare them in

rollbackFor

to guarantee rollback.

<code>@Transactional(rollbackFor = CustomException.class)
public String testMerge() throws Exception {
    // ...
    throw new CustomException(e);
}
</code>

13. Nested transaction handling

To prevent a failure in a nested transactional method from rolling back the outer transaction, catch the exception inside the outer method and handle it without re‑throwing.

<code>@Transactional
public String testMerge() {
    testAService.testA();
    try {
        testBService.testB();
    } catch (Exception e) {
        log.error("testMerge error:{}", e);
    }
    return "ok";
}
</code>
backendJavaAOPSpringTransactionalExceptionHandling
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

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