SpringBoot: Declarative @Transactional vs Programmatic TransactionTemplate
The article compares SpringBoot's declarative transaction management using @Transactional with programmatic transaction handling via TransactionTemplate, explaining their underlying mechanisms, code examples, advantages, disadvantages, and suitable scenarios, and provides a detailed side‑by‑side table to help developers choose the appropriate approach.
Declarative vs. Programmatic Transactions
Spring Boot supports two transaction management styles:
Declarative Transaction
Apply @Transactional on a class or method. Spring creates a JDK or CGLIB proxy that starts, commits, or rolls back a transaction automatically.
Key characteristics
Non‑intrusive
Simple configuration
Concise code
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO orderDTO) {
stockMapper.decreaseStock(orderDTO.getProductId(), orderDTO.getQuantity());
orderMapper.insert(buildOrder(orderDTO));
userMapper.decreaseBalance(orderDTO.getUserId(), orderDTO.getTotalAmount());
}
}Programmatic Transaction
Manually invoke Spring’s transaction API, giving full control over transaction boundaries, commit, and rollback.
Key characteristics
Intrusive
More complex configuration
Verbose code
High flexibility for nested transactions or dynamic transaction switches
@Service
public class OrderService {
@Autowired
private TransactionTemplate transactionTemplate;
public void createOrder(OrderDTO orderDTO) {
transactionTemplate.execute(status -> {
try {
stockMapper.decreaseStock(orderDTO.getProductId(), orderDTO.getQuantity());
orderMapper.insert(buildOrder(orderDTO));
userMapper.decreaseBalance(orderDTO.getUserId(), orderDTO.getTotalAmount());
return true; // success, transaction commits automatically
} catch (Exception e) {
status.setRollbackOnly(); // failure, transaction rolls back
throw new RuntimeException(e.getMessage());
}
});
}
}Spring Boot provides two implementations for programmatic transactions: TransactionTemplate (recommended) and PlatformTransactionManager (low‑level, rarely used).
Underlying Mechanism Comparison
Declarative Transaction Flow
During container startup, Spring scans for @Transactional annotations and creates a proxy for each target class or method.
Method invocation goes through the proxy.
The proxy opens a transaction via PlatformTransactionManager and sets transaction attributes (propagation, isolation, etc.).
The business logic executes.
If the method returns normally, the proxy commits the transaction.
If an exception is thrown, the proxy rolls back according to the rollbackFor configuration.
Programmatic Transaction Flow
Developer calls TransactionTemplate (or PlatformTransactionManager) to start a transaction.
Business logic runs inside the transaction scope.
Developer decides to commit or roll back based on execution result.
After execution, resources are released (automatically for TransactionTemplate).
Dimension‑by‑Dimension Comparison
Usage : Declarative – annotate with @Transactional (or XML). Programmatic – write code with TransactionTemplate to control start, commit, rollback.
Code Intrusiveness : Declarative – non‑intrusive (annotation only). Programmatic – intrusive (transaction code mixed with business logic).
Flexibility : Declarative – low (transaction attributes fixed at configuration time). Programmatic – high (dynamic control of transaction switch, commit/rollback timing, supports complex nesting).
Configuration Complexity : Declarative – very low (single data source needs no extra config). Programmatic – higher (need to inject TransactionTemplate and write execution logic).
Transaction Granularity : Declarative – coarse (method or class level). Programmatic – fine (any code block inside a method can be wrapped).
Failure Risk : Declarative – higher (AOP proxy issues such as private methods, self‑invocation, missing rollbackFor). Programmatic – very low (manual control eliminates proxy‑related failures).
Performance : Declarative – slightly lower due to AOP proxy overhead (negligible). Programmatic – slightly higher because there is no proxy overhead.
Applicable Scenarios : Declarative – most common cases (single transaction, simple business logic, prioritize code simplicity). Programmatic – complex cases (nested transactions, dynamic transaction control, fine‑grained transaction boundaries).
Complete Usage Examples
Environment Preparation (single data source)
Import Spring Boot 2.x+ dependencies and configure application.yml data source (same as the previous article, omitted here).
Declarative Transaction Example
Requirement: order creation (decrease stock, create order, decrease balance) must be atomic; logging uses an independent transaction.
@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 = buildOrder(orderDTO);
orderMapper.insert(order);
int userRows = userMapper.decreaseBalance(orderDTO.getUserId(), orderDTO.getTotalAmount());
if (userRows == 0) {
throw new BusinessException("用户余额不足");
}
// Independent log transaction (REQUIRES_NEW)
orderLogService.recordOrderLog(order.getOrderNo(), "下单成功");
} catch (Exception e) {
log.error("下单失败:{}", e.getMessage(), e);
throw new RuntimeException(e.getMessage());
}
}
private Order buildOrder(OrderDTO dto) {
Order order = new Order();
order.setUserId(dto.getUserId());
order.setProductId(dto.getProductId());
order.setQuantity(dto.getQuantity());
order.setOrderNo(UUID.randomUUID().toString().replace("-", ""));
order.setCreateTime(new Date());
return order;
}
}
@Service
public class OrderLogService {
@Autowired
private OrderLogMapper orderLogMapper;
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public void recordOrderLog(String orderNo, String content) {
OrderLog log = new OrderLog();
log.setOrderNo(orderNo);
log.setContent(content);
log.setCreateTime(new Date());
orderLogMapper.insert(log);
}
}Programmatic Transaction Example
Requirement: order creation where the second step (balance deduction) may be optional based on a runtime flag.
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StockMapper stockMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private TransactionTemplate transactionTemplate;
// Step 1: stock decrease and order creation (must be transactional)
public void createOrder(OrderDTO orderDTO, boolean needBalanceTransaction) {
Order order = transactionTemplate.execute(status -> {
try {
int stockRows = stockMapper.decreaseStock(orderDTO.getProductId(), orderDTO.getQuantity());
if (stockRows == 0) {
status.setRollbackOnly();
throw new BusinessException("库存不足");
}
Order newOrder = buildOrder(orderDTO);
orderMapper.insert(newOrder);
return newOrder; // commit and return order
} catch (Exception e) {
status.setRollbackOnly();
log.error("扣库存、生成订单失败:{}", e.getMessage());
throw new RuntimeException(e.getMessage());
}
});
// Step 2: optional balance deduction
if (needBalanceTransaction) {
transactionTemplate.execute(status -> {
try {
int userRows = userMapper.decreaseBalance(orderDTO.getUserId(), orderDTO.getTotalAmount());
if (userRows == 0) {
status.setRollbackOnly();
throw new BusinessException("用户余额不足");
}
return true;
} catch (Exception e) {
status.setRollbackOnly();
log.error("扣减余额失败:{}", e.getMessage());
throw new RuntimeException(e.getMessage());
}
});
} else {
// Non‑transactional balance deduction
userMapper.decreaseBalance(orderDTO.getUserId(), orderDTO.getTotalAmount());
}
}
private Order buildOrder(OrderDTO dto) {
Order order = new Order();
order.setUserId(dto.getUserId());
order.setProductId(dto.getProductId());
order.setQuantity(dto.getQuantity());
order.setOrderNo(UUID.randomUUID().toString().replace("-", ""));
order.setCreateTime(new Date());
return order;
}
}Precautions
Declarative Transaction
Do not place @Transactional on private or protected methods (AOP cannot proxy them).
Avoid self‑invocation (e.g., this.method()) because the call bypasses the proxy.
Configure rollbackFor = Exception.class to ensure checked exceptions trigger rollback.
Programmatic Transaction
Call status.setRollbackOnly() when business logic fails; otherwise the transaction will commit.
If using PlatformTransactionManager directly, release resources manually to avoid leaks.
Do not overuse programmatic transactions for simple scenarios, as they increase code complexity and reduce development efficiency.
Conclusion
Declarative transactions are simple, efficient, and non‑intrusive, making them the default choice for most business logic. Programmatic transactions provide fine‑grained control and lower failure risk, suitable for complex or dynamic transaction requirements. The guiding principle is to use declarative transactions whenever they satisfy the requirements; switch to programmatic only when necessary, while observing the listed pitfalls to ensure data consistency.
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.
