Common Scenarios Where Spring Transactions Fail and How to Fix Them

This article explains why Spring @Transactional may become ineffective in various situations—such as wrong method visibility, final or static modifiers, internal method calls, missing bean registration, multithreading, unsupported table engines, misconfigured propagation, swallowed exceptions, and improper rollback settings—and provides practical solutions for each case.

Wukong Talks Architecture
Wukong Talks Architecture
Wukong Talks Architecture
Common Scenarios Where Spring Transactions Fail and How to Fix Them

Preface

Spring transaction management is a core skill for Java developers, but misuse can easily cause transactions to become ineffective, leading to data inconsistency.

1. Transaction Not Effective

1.1 Access Modifier Issue

If a transactional method is declared private, protected or package‑private, Spring will not create a proxy for it because

AbstractFallbackTransactionAttributeSource.computeTransactionAttribute

only returns a TransactionAttribute for public methods.

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

1.2 Method Declared final

A final method cannot be overridden by the CGLIB proxy, so the transactional advice cannot be applied.

@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 Calls

Calling another transactional method from the same class (e.g., this.updateStatus()) bypasses the proxy, so the second method runs without a transaction.

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
        updateStatus(userModel);
    }

    @Transactional
    public void updateStatus(UserModel userModel) {
        doSameThing();
    }
}

Solutions include extracting the called method to another @Service bean, injecting the bean into itself, or using AopContext.currentProxy() to obtain the proxy.

1.3.1 Add a New Service

@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();
    }
}

1.3.2 Inject Self

@Service
public class ServiceA {
    @Autowired
    private ServiceA serviceA;

    public void save(User user) {
        queryData1();
        queryData2();
        serviceA.doSave(user);
    }

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

1.3.3 Use AopContext

@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 Bean Not Managed by Spring

If a class lacks @Service, @Component, etc., Spring will not create a bean, and any @Transactional annotation on its methods will be ignored.

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

1.5 Multithreaded Calls

Transactional code executed in a new thread does not share the same database connection, so the outer transaction cannot roll back the inner thread’s work.

@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");
    }
}

1.6 Table Engine Does Not Support Transactions

MyISAM tables cannot participate in transactions; using InnoDB (or another transactional engine) is required for multi‑table operations.

CREATE TABLE `category` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  `one_category` VARCHAR(20) COLLATE utf8mb4_bin DEFAULT NULL,
  ...
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

1.7 Transaction Not Enabled

In a plain Spring project you must declare a DataSourceTransactionManager and appropriate aop configuration; Spring Boot does this automatically.

<!-- 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>
<aop:config>
    <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/>
    <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config>

2. Transaction Does Not Roll Back

2.1 Wrong Propagation Setting

Using Propagation.NEVER disables transaction creation, causing the method to run without a transaction.

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

2.2 Swallowing Exceptions

If a transactional method catches exceptions and does not re‑throw them, Spring assumes the operation succeeded and will not roll back.

@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 Non‑Runtime Exceptions

By default Spring rolls back only on RuntimeException or Error. Throwing a checked Exception will not trigger a rollback unless configured.

@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 never occurs, the transaction will not roll back for the actual database exceptions.

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

2.5 Nested Transaction Rollback Propagation

When a nested transaction throws an exception that propagates outward, the outer transaction also rolls back. To isolate the inner rollback, catch the exception inside the outer method.

@Service
public class UserService {
    @Autowired
    private RoleService roleService;

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

@Service
public class RoleService {
    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
        // ...
    }
}

3. Other Topics

3.1 Large Transaction Problems

Annotating whole service methods with @Transactional can unintentionally include many read‑only queries, leading to long‑running transactions and performance issues. It is better to limit the transactional scope to the exact statements that modify data.

3.2 Programmatic Transactions

Spring also offers programmatic transaction control via TransactionTemplate, which avoids AOP proxy pitfalls and gives finer‑grained control.

@Autowired
private TransactionTemplate transactionTemplate;

public void save(final User user) {
    queryData1();
    queryData2();
    transactionTemplate.execute(status -> {
        addData1();
        updateData2();
        return Boolean.TRUE;
    });
}
It is advisable to prefer programmatic transactions for complex scenarios, while simple, stable operations can still safely use @Transactional.
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.

Backendtransactiontransactionaltransaction-management
Wukong Talks Architecture
Written by

Wukong Talks Architecture

Explaining distributed systems and architecture through stories. Author of the "JVM Performance Tuning in Practice" column, open-source author of "Spring Cloud in Practice PassJava", and independently developed a PMP practice quiz mini-program.

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.