Spring Boot
Transactional Annotation
Spring Framework
Database Transactions
Java Programming

Transactional annotation not working in Spring Boot

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

When @Transactional appears not to work in Spring Boot, the problem is usually not that Spring forgot how to start transactions. It is usually a proxy or configuration issue: the method is called the wrong way, the exception type does not trigger rollback, or the bean is not actually being managed through Spring’s transactional proxy.

@Transactional Works Through a Proxy

Spring transaction management is usually applied by wrapping your bean in a proxy. That proxy opens the transaction before the method call and commits or rolls back afterward.

That means the transaction advice only runs when the call goes through the Spring-managed proxy.

A normal working example:

java
1import org.springframework.stereotype.Service;
2import org.springframework.transaction.annotation.Transactional;
3
4@Service
5public class OrderService {
6
7    @Transactional
8    public void placeOrder() {
9        // write to database
10    }
11}

This works when another bean calls orderService.placeOrder() through the Spring container.

Self-Invocation Is the Classic Trap

A very common failure case is calling a transactional method from another method in the same class.

java
1@Service
2public class OrderService {
3
4    public void createAndSave() {
5        saveInternal();
6    }
7
8    @Transactional
9    public void saveInternal() {
10        // expected transaction may not start here
11    }
12}

This is called self-invocation. The call does not go through the proxy, so the transactional advice is skipped.

Typical fixes are:

  • move the transactional method to another Spring-managed bean
  • call the method through a proxied bean instead of this
  • redesign the service boundary so the transactional entry point is external

Method Visibility Matters

Transactional methods are typically expected to be public in common proxy-based setups. If the method is private, Spring cannot usually apply the transactional advice the way developers expect.

Bad example:

java
@Transactional
private void saveOrder() {
}

Better:

java
@Transactional
public void saveOrder() {
}

Even when some proxy variants can handle more than public methods, public transactional boundaries are still the clearest and least surprising design.

Rollback Rules Surprise Many People

By default, Spring rolls back on unchecked exceptions such as RuntimeException, but not on every checked exception automatically.

java
1@Transactional
2public void createOrder() throws Exception {
3    // database writes
4    throw new Exception("checked exception");
5}

This may not roll back the way you expect.

If you need rollback for checked exceptions, declare it explicitly:

java
1@Transactional(rollbackFor = Exception.class)
2public void createOrder() throws Exception {
3    // database writes
4    throw new Exception("checked exception");
5}

This is one of the most common reasons developers think transactions are “not working” when they are actually being committed according to Spring’s default rules.

Make Sure the Bean Is Managed by Spring

If you instantiate the class manually with new, Spring cannot apply transactions.

Wrong:

java
OrderService service = new OrderService();
service.placeOrder();

Correct usage is dependency injection:

java
1@RestController
2public class OrderController {
3    private final OrderService orderService;
4
5    public OrderController(OrderService orderService) {
6        this.orderService = orderService;
7    }
8}

If the bean is not created by the container, the proxy does not exist.

Check the Transaction Manager and Persistence Setup

Spring Boot usually auto-configures transaction management correctly when you use standard data starters, but the transaction manager still has to match the persistence technology you are using.

If the application has multiple data sources or a nonstandard setup, verify:

  • the correct transaction manager is present
  • the repository or entity manager uses the same data source
  • the transactional method is actually hitting the expected persistence context

These issues are less common than self-invocation, but they matter in more complex applications.

Common Pitfalls

The most common mistake is self-invocation, where a method inside the same class calls the transactional method directly and bypasses the proxy. Another is expecting rollback for checked exceptions without configuring rollbackFor. Developers also often put @Transactional on private methods or on classes they instantiate manually instead of letting Spring manage them. A final issue is assuming every data source and repository is automatically wired to the same transaction manager in multi-database setups.

Summary

  • '@Transactional usually works through a Spring proxy, not by magic inside the class itself.'
  • Self-invocation is the most common reason transactional advice is skipped.
  • Public transactional entry points are the safest design.
  • By default, rollback behavior differs between unchecked and checked exceptions.
  • Make sure the bean and the transaction manager are actually managed by Spring.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.