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.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
SpringBoot: Declarative @Transactional vs Programmatic TransactionTemplate

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.

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.

AOPSpringBootTransaction ManagementProgrammatic TransactionDeclarative TransactionTransactionTemplate
Java Tech Workshop
Written by

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.

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.