Deep Dive into SpringBoot @Transactional: How It Works and Common Pitfalls
This article explains the fundamentals of transaction management in SpringBoot, detailing the ACID properties, the @Transactional annotation’s AOP‑based implementation, core attributes such as propagation, isolation, rollback rules, timeout and read‑only mode, and provides practical code examples for single‑ and multi‑datasource scenarios.
Why Transaction Management Matters
In enterprise applications, data consistency is essential. For example, an order operation must deduct inventory, create the order, and deduct the user's balance atomically; any failure should roll back all changes to avoid bugs such as "stock deducted but order not created".
What Is a Transaction?
A transaction is the smallest indivisible unit of database work. It either completes fully or rolls back entirely, guaranteeing the four ACID properties:
Atomicity : all operations succeed or none do.
Consistency : database constraints remain valid before and after execution.
Isolation : concurrent transactions do not interfere with each other.
Durability : once committed, changes survive crashes.
SpringBoot implements transaction management using Spring AOP dynamic proxies (JDK or CGLIB) that weave transaction start, commit, and rollback logic around the target method.
SpringBoot Transaction Types
SpringBoot supports two styles: programmatic transactions (e.g., TransactionTemplate) and annotation‑driven transactions using @Transactional. The annotation approach is non‑intrusive and covers the majority of enterprise use cases.
Underlying Mechanism of @Transactional
When the Spring container starts, it scans for @Transactional annotations and creates a proxy for each annotated class or method. The proxy workflow is:
During container initialization, a dynamic proxy is generated for the annotated bean.
When the method is invoked, the call goes through the proxy.
The proxy asks the TransactionManager to start a transaction, acquiring a connection and setting isolation and propagation settings.
The target method’s business logic runs (e.g., deduct inventory, create order).
If the method returns normally, the proxy commits the transaction via the TransactionManager.
If an exception matching the rollback rules is thrown, the proxy rolls back the transaction.
If the exception does not match the rollback criteria, the transaction is committed.
Transaction Manager
SpringBoot automatically configures a TransactionManager based on the datasource:
For JDBC or MyBatis: DataSourceTransactionManager.
For JPA: JpaTransactionManager.
In multi‑datasource projects, developers must define multiple managers and reference them explicitly.
Key @Transactional Attributes
1. value / transactionManager
Both specify the bean name of the TransactionManager, required only in multi‑datasource scenarios.
@Transactional(transactionManager = "db1TransactionManager")
public void addOrder(Order order) {
// business logic
}2. propagation
Defines how a transaction behaves when a transactional method calls another transactional method. The most common settings are:
REQUIRED (default) : join existing transaction or create a new one.
REQUIRES_NEW : always start a new transaction, suspending the current one.
NESTED : create a nested sub‑transaction; rollback of the sub‑transaction does not affect the outer one.
MANDATORY : must run inside an existing transaction, otherwise throws an exception.
NEVER : must run without a transaction.
Misconfiguring propagation (e.g., using REQUIRES_NEW for a logging call) is a frequent cause of transaction‑failure bugs.
3. isolation
Controls how concurrent transactions see each other's changes. SpringBoot defaults to READ_COMMITTED. The main isolation levels are: READ_UNCOMMITTED: allows dirty reads. READ_COMMITTED: prevents dirty reads. REPEATABLE_READ: prevents dirty and non‑repeatable reads; MySQL also prevents phantom reads via MVCC. SERIALIZABLE: fully isolates transactions but incurs severe performance penalties.
Recommendation: use READ_COMMITTED for most cases; switch to REPEATABLE_READ for high‑consistency domains such as finance.
4. rollbackFor / noRollbackFor
Control which exceptions trigger a rollback. By default, Spring rolls back on unchecked (RuntimeException) exceptions only. rollbackFor: forces rollback for specified checked exceptions. noRollbackFor: prevents rollback for specified unchecked exceptions.
@Transactional(rollbackFor = Exception.class)
public void addOrder(Order order) throws IOException {
// business logic
}
@Transactional(rollbackFor = Exception.class, noRollbackFor = BusinessException.class)
public void updateOrder(Order order) {
// business logic
}5. timeout
Specifies the maximum execution time (seconds) before the transaction is automatically rolled back.
@Transactional(rollbackFor = Exception.class, timeout = 3)
public void batchImportData(List<Data> dataList) {
// time‑consuming import
}6. readOnly
Marks a transaction as read‑only, allowing the database to apply optimizations and preventing accidental writes.
@Transactional(readOnly = true)
public List<Order> getOrderList(Long userId) {
return orderMapper.selectByUserId(userId);
}Practical Example: Order Service
The article walks through a complete order‑processing scenario where three operations (stock deduction, order creation, balance deduction) must succeed together, while order‑log insertion runs in an independent transaction (REQUIRES_NEW) to guarantee log persistence even if the main transaction rolls back.
@Service
@Slf4j
public class OrderService {
@Autowired private OrderMapper orderMapper;
@Autowired private StockMapper stockMapper;
@Autowired private UserMapper userMapper;
@Autowired private OrderLogService orderLogService;
@Transactional(rollbackFor = Exception.class, timeout = 5)
public void createOrder(OrderDTO orderDTO) {
try {
int stockRows = stockMapper.decreaseStock(orderDTO.getProductId(), orderDTO.getQuantity());
if (stockRows == 0) throw new BusinessException("库存不足");
Order order = new Order();
order.setUserId(orderDTO.getUserId());
order.setProductId(orderDTO.getProductId());
order.setQuantity(orderDTO.getQuantity());
order.setOrderNo(UUID.randomUUID().toString().replace("-", ""));
order.setCreateTime(new Date());
orderMapper.insert(order);
int userRows = userMapper.decreaseBalance(orderDTO.getUserId(), orderDTO.getTotalAmount());
if (userRows == 0) throw new BusinessException("用户余额不足");
orderLogService.recordOrderLog(order.getOrderNo(), "下单成功");
} catch (Exception e) {
log.error("下单失败:{}", e.getMessage(), e);
throw new RuntimeException(e.getMessage());
}
}
}
@Service
@Slf4j
public class OrderLogService {
@Autowired private OrderLogMapper orderLogMapper;
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public void recordOrderLog(String orderNo, String content) {
OrderLog orderLog = new OrderLog();
orderLog.setOrderNo(orderNo);
orderLog.setContent(content);
orderLog.setCreateTime(new Date());
orderLogMapper.insert(orderLog);
}
}Test cases demonstrate normal success, stock‑insufficient rollback, and balance‑insufficient rollback, with the log entry persisting in both failure scenarios.
Multi‑Datasource Transaction Configuration
When an application uses separate databases (e.g., order DB and user DB), each requires its own PlatformTransactionManager. The service methods then reference the appropriate manager via the transactionManager attribute.
@Configuration
public class TransactionConfig {
@Bean(name = "orderTransactionManager")
public PlatformTransactionManager orderTransactionManager(@Qualifier("orderDataSource") DataSource ds) {
return new DataSourceTransactionManager(ds);
}
@Bean(name = "userTransactionManager")
public PlatformTransactionManager userTransactionManager(@Qualifier("userDataSource") DataSource ds) {
return new DataSourceTransactionManager(ds);
}
}
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class, transactionManager = "orderTransactionManager")
public void createOrder(Order order) { orderMapper.insert(order); }
}
@Service
public class UserService {
@Transactional(rollbackFor = Exception.class, transactionManager = "userTransactionManager")
public void decreaseBalance(Long userId, BigDecimal amount) { userMapper.decreaseBalance(userId, amount); }
}Key Takeaways
Understand that @Transactional works via Spring AOP proxies and a TransactionManager; the proxy is what actually activates the transaction.
Configure core attributes (propagation, isolation, rollback rules) according to business requirements.
Be aware of common pitfalls such as incorrect propagation settings, missing rollbackFor for checked exceptions, and unnecessary use of read‑only transactions on write methods.
The most frequently used configuration in real projects is:
@Transactional(rollbackFor = Exception.class)Additional attributes (propagation, timeout, readOnly) can be added as needed to meet specific consistency or performance goals.
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.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
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.
