Back to @Transactional Basics
Q2Question 2 of 7

Declarative vs Programmatic transaction management?

Key Concepts
  • Declarative uses @Transactional annotation where the AOP proxy manages begin, commit, and rollback automatically
  • Programmatic uses TransactionTemplate or PlatformTransactionManager for explicit transaction control in code
  • Declarative is cleaner and suits most single-operation use cases; programmatic gives fine-grained boundary control
  • TransactionTemplate.execute() returns a value; executeWithoutResult() handles void operations
  • RuntimeException triggers auto-rollback; checked exceptions commit by default in both approaches
  • Use programmatic for batch commits, self-invocation workarounds, and mixed TX and non-TX logic in one method

Definition

Declarative transaction management uses @Transactional and lets the AOP proxy handle everything, while programmatic management uses TransactionTemplate for explicit begin, commit, and rollback control in code.
  • Declarative (@Transactional) – annotate a method or class and let the AOP proxy manage the transaction lifecycle; cleanest approach for single business operations.
  • Programmatic (TransactionTemplate) – wrap transactional code inside template.execute(status -> { ... }) for explicit control over where the transaction starts and ends.
  • PlatformTransactionManager – the lowest-level API; you call getTransaction(), commit(), and rollback() yourself; rarely needed because TransactionTemplate wraps it.
  • Rollback Rules – in both approaches, a RuntimeException auto-rolls-back and a checked exception commits by default; use rollbackFor (declarative) or status.setRollbackOnly() (programmatic) to override.
  • When to choose programmatic – batch processing with per-chunk commits, self-invocation scenarios, or mixing transactional and non-transactional work in one method.

At the National Bank of India, declarative is like the automatic vault door that opens when Rajesh's form arrives, processes the transfer, and closes when done. Programmatic is like Rajesh himself standing at the counter, saying "open the ledger now," writing two entries (debit SAV2001, credit SAV3002), and then saying "close and stamp the ledger." The automatic door is easier, but when Rajesh needs to commit partial batches separately, he takes manual control.

Understanding the Concepts

What is declarative transaction management?

Declarative transaction management means you annotate a method with @Transactional and let the AOP proxy start, commit, and roll back the transaction around it. You write only business logic; Spring handles the connection lifecycle. This is the standard and most common approach in Spring Boot applications.

JAVA
package com.bank.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;

@Service
public class TransferService {

    private final AccountRepository accountRepo;

    public TransferService(AccountRepository accountRepo) {
        this.accountRepo = accountRepo;
    }

    @Transactional(rollbackFor = Exception.class)
    public void transfer(String fromId, String toId, BigDecimal amount) {
        Account sender = accountRepo.findByIdForUpdate(fromId);
        Account receiver = accountRepo.findByIdForUpdate(toId);
        sender.debit(amount);
        receiver.credit(amount);
        accountRepo.save(sender);
        accountRepo.save(receiver);
    }
}

The proxy calls setAutoCommit(false) before the method, calls commit() on normal return, and calls rollback() if a RuntimeException propagates out.

What is programmatic transaction management?

Programmatic transaction management means you control the transaction boundary yourself using TransactionTemplate or PlatformTransactionManager. You wrap the transactional code inside a callback, and the template manages begin, commit, and rollback for that specific block.

JAVA
package com.bank.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;

@Service
public class ProgrammaticTransferService {

    private final TransactionTemplate txTemplate;
    private final AccountRepository accountRepo;

    public ProgrammaticTransferService(
            PlatformTransactionManager txManager,
            AccountRepository accountRepo) {
        this.txTemplate = new TransactionTemplate(txManager);
        this.accountRepo = accountRepo;
    }

    public BigDecimal transferAndReturnBalance(
            String fromId, String toId, BigDecimal amount) {
        return txTemplate.execute(status -> {
            Account sender = accountRepo.findByIdForUpdate(fromId);
            Account receiver = accountRepo.findByIdForUpdate(toId);
            sender.debit(amount);
            receiver.credit(amount);
            accountRepo.save(sender);
            accountRepo.save(receiver);
            return sender.getBalance();
        });
    }
}

The TransactionTemplate is thread-safe. Create it once in the constructor and reuse it for every call.

How does TransactionTemplate compare to @Transactional?

Both achieve the same result: atomic database operations. The difference is where the transaction boundary is defined.

| Aspect | @Transactional (Declarative) | TransactionTemplate (Programmatic) |
|--------|-----------------------------|------------------------------------|
| Boundary | Entire method | Lambda block only |
| Mechanism | AOP proxy intercept | Direct template call |
| Self-invocation | Broken (proxy bypassed) | Works (no proxy involved) |
| Multiple TXs in one method | Not possible | Each execute() is independent |
| Readability | Clean, one annotation | More verbose, explicit |
| Configuration | Annotation attributes | Template setter methods |
| Rollback | Exception propagation | Exception or setRollbackOnly() |

JAVA
package com.bank.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;

@Service
public class ComparisonService {

    private final TransactionTemplate txTemplate;
    private final AccountRepository accountRepo;

    public ComparisonService(
            PlatformTransactionManager txManager,
            AccountRepository accountRepo) {
        this.txTemplate = new TransactionTemplate(txManager);
        this.accountRepo = accountRepo;
    }

    // Declarative: entire method is one transaction
    @Transactional
    public void transferDeclarative(String fromId, String toId, BigDecimal amount) {
        Account sender = accountRepo.findByIdForUpdate(fromId);
        Account receiver = accountRepo.findByIdForUpdate(toId);
        sender.debit(amount);
        receiver.credit(amount);
    }

    // Programmatic: only the lambda block is transactional
    public void transferProgrammatic(String fromId, String toId, BigDecimal amount) {
        log.info("Starting transfer"); // outside the transaction
        txTemplate.executeWithoutResult(status -> {
            Account sender = accountRepo.findByIdForUpdate(fromId);
            Account receiver = accountRepo.findByIdForUpdate(toId);
            sender.debit(amount);
            receiver.credit(amount);
        });
        log.info("Transfer committed"); // outside the transaction
    }
}

What is execute vs executeWithoutResult?

The execute method takes a TransactionCallback and returns a value of type T. Use it when the transactional block must produce a result.

The executeWithoutResult method takes a Consumer and returns void. It was added in Spring 5.2 to avoid the pattern of execute(status -> { ...; return null; }).

JAVA
package com.bank.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;

@Service
public class AccountQueryService {

    private final TransactionTemplate txTemplate;
    private final AccountRepository accountRepo;

    public AccountQueryService(
            PlatformTransactionManager txManager,
            AccountRepository accountRepo) {
        this.txTemplate = new TransactionTemplate(txManager);
        this.accountRepo = accountRepo;
    }

    // execute: returns a value
    public BigDecimal getBalanceInTx(String accountId) {
        return txTemplate.execute(status -> {
            Account account = accountRepo.findById(accountId);
            return account.getBalance();
        });
    }

    // executeWithoutResult: void operation
    public void freezeAccount(String accountId) {
        txTemplate.executeWithoutResult(status -> {
            Account account = accountRepo.findById(accountId);
            account.setStatus("FROZEN");
            accountRepo.save(account);
        });
    }
}

How does programmatic TX handle batch processing?

In batch processing, a single @Transactional around the entire batch means one failure rolls back everything. TransactionTemplate solves this by making each chunk an independent transaction. If chunk 3 fails, chunks 1 and 2 are already committed.

JAVA
package com.bank.batch;

import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;
import java.util.List;

@Service
public class InterestBatchService {

    private final TransactionTemplate txTemplate;
    private final AccountRepository accountRepo;
    private static final int BATCH_SIZE = 500;

    public InterestBatchService(
            PlatformTransactionManager txManager,
            AccountRepository accountRepo) {
        this.txTemplate = new TransactionTemplate(txManager);
        this.accountRepo = accountRepo;
    }

    public void applyMonthlyInterest(BigDecimal rate) {
        List<Account> allAccounts = accountRepo.findAllSavings();
        int total = allAccounts.size();
        int committed = 0;

        for (int i = 0; i < total; i += BATCH_SIZE) {
            int start = i;
            int end = Math.min(i + BATCH_SIZE, total);
            List<Account> chunk = allAccounts.subList(start, end);

            try {
                txTemplate.executeWithoutResult(status -> {
                    for (Account acct : chunk) {
                        BigDecimal interest = acct.getBalance()
                            .multiply(rate);
                        acct.credit(interest);
                        accountRepo.save(acct);
                    }
                });
                committed += chunk.size();
            } catch (Exception e) {
                log.error("Batch {} failed, skipping", start / BATCH_SIZE, e);
                // Earlier batches are already committed
            }
        }
        log.info("Interest applied to {} of {} accounts", committed, total);
    }
}
TEXT
Interest applied to 49500 of 50000 accounts

What are the rollback rules?

Both declarative and programmatic share the same default: RuntimeException and Error trigger rollback; checked exceptions trigger commit.

In declarative mode, override with rollbackFor:

JAVA
@Transactional(rollbackFor = Exception.class)
public void transferDeclarative(String fromId, String toId, BigDecimal amount)
        throws TransferException {
    // checked TransferException now triggers rollback
}

In programmatic mode, override with status.setRollbackOnly():

JAVA
txTemplate.executeWithoutResult(status -> {
    Account sender = accountRepo.findById(fromId);
    if (sender.getBalance().compareTo(amount) < 0) {
        status.setRollbackOnly(); // force rollback without throwing
        return;
    }
    sender.debit(amount);
    accountRepo.save(sender);
});

What are common mistakes?

Mistake 1: using @Transactional when batch needs per-chunk commits

JAVA
// WRONG: one failure rolls back all 50,000 accounts
@Transactional
public void applyInterestBad(BigDecimal rate) {
    for (Account acct : accountRepo.findAllSavings()) {
        BigDecimal interest = acct.getBalance().multiply(rate);
        acct.credit(interest);
        accountRepo.save(acct);
    }
}
JAVA
// CORRECT: use TransactionTemplate for per-chunk commits
public void applyInterestCorrect(BigDecimal rate) {
    List<Account> accounts = accountRepo.findAllSavings();
    for (int i = 0; i < accounts.size(); i += BATCH_SIZE) {
        List<Account> chunk = accounts.subList(i, Math.min(i + BATCH_SIZE, accounts.size()));
        txTemplate.executeWithoutResult(status -> {
            for (Account acct : chunk) {
                acct.credit(acct.getBalance().multiply(rate));
                accountRepo.save(acct);
            }
        });
    }
}

Mistake 2: mixing both approaches redundantly

JAVA
// WRONG: @Transactional already manages the TX, TransactionTemplate creates a nested one
@Transactional
public void doubleManaged() {
    txTemplate.executeWithoutResult(status -> {
        // this starts a second transaction if propagation is REQUIRES_NEW
    });
}

Pick one approach per method. Do not combine declarative and programmatic on the same boundary.

When should you use programmatic over declarative?

Use programmatic when:

  • Batch processing requires per-chunk commits so earlier progress survives a later failure.
  • Self-invocation: a method calls another method on the same bean, and you need the inner call to have its own transaction.
  • Mixed logic: part of the method is transactional and part is not, and you need the boundary at a finer grain than the whole method.
  • Dynamic rollback: you want to decide at runtime whether to roll back based on business rules without throwing an exception.

Use declarative for everything else. It is cleaner, less error-prone, and the standard approach.

JAVA
// Programmatic solves self-invocation
@Service
public class SettlementService {

    private final TransactionTemplate txTemplate;
    private final TradeRepository tradeRepo;

    public SettlementService(
            PlatformTransactionManager txManager, TradeRepository tradeRepo) {
        this.txTemplate = new TransactionTemplate(txManager);
        this.tradeRepo = tradeRepo;
    }

    // No proxy needed: each trade settles in its own TX
    public void settleAll() {
        List<Trade> pending = tradeRepo.findPending();
        for (Trade trade : pending) {
            txTemplate.executeWithoutResult(status -> {
                trade.settle();
                tradeRepo.save(trade);
            });
        }
    }
}

What should a senior engineer know?

  • TransactionTemplate wraps PlatformTransactionManager and is the recommended programmatic API. Using PlatformTransactionManager directly with getTransaction(), commit(), and rollback() is rarely needed.
  • The template is thread-safe once configured. Create it once per service in the constructor.
  • Configure propagation, isolation, timeout, and readOnly on the template with setter methods.
  • In reactive applications (WebFlux), use TransactionalOperator instead of TransactionTemplate because reactive code does not have ThreadLocal-bound connections.
  • Prefer declarative for clarity and reserve programmatic for batch commits, self-invocation fixes, and dynamic boundaries where the annotation cannot express what you need.
JAVA
package com.bank.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;

@Configuration
public class TxTemplateConfig {

    @Bean
    public TransactionTemplate batchTxTemplate(PlatformTransactionManager txManager) {
        TransactionTemplate template = new TransactionTemplate(txManager);
        template.setPropagationBehavior(
            TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        template.setTimeout(30);
        return template;
    }
}

Visual Overview

DIAGRAM
  Declarative (@Transactional)          Programmatic (TransactionTemplate)
  ─────────────────────────────          ───────────────────────────────────

  Caller                                 Caller
    │                                      │
    ▼                                      ▼
  ┌─────────────┐                       method() {
  │  AOP Proxy  │                         log("start");
  │  begin TX   │                         txTemplate.execute(status -> {
  │  ┌────────┐ │                           ┌────────────────────┐
  │  │ method │ │                           │  begin TX           │
  │  │  body  │ │                           │  run lambda body    │
  │  └────────┘ │                           │  commit or rollback │
  │  commit/    │                           └────────────────────┘
  │  rollback   │                         });
  └─────────────┘                         log("done");
                                        }

Key Takeaway

Declarative transaction management with @Transactional is the standard approach in Spring Boot. You annotate the method, and the AOP proxy handles begin, commit, and rollback. It is clean, requires one line of code, and is sufficient for most single business operations like transferring money from Rajesh's account SAV2001 to Priya's account SAV3002.

Programmatic transaction management with TransactionTemplate gives explicit control. You call execute with a lambda, and the template manages the transaction boundary for that specific block. Each execute call is an independent transaction that commits or rolls back on its own. In the National Bank of India's month-end interest batch, processing 50,000 savings accounts in a single @Transactional means one failure rolls back everything. TransactionTemplate solves this by processing in batches of 500 where each batch commits independently. If batch 3 fails, batches 1 and 2 keep their progress.

The rollback rules are the same for both: RuntimeException and Error trigger rollback; checked exceptions commit by default. Declarative overrides this with rollbackFor, while programmatic uses status.setRollbackOnly(). The choice between approaches depends on the use case: declarative for the common single-operation path, programmatic for batch commits, self-invocation workarounds, and fine-grained boundaries.

Remember one sentence: @Transactional is declarative and clean for single operations, while TransactionTemplate is programmatic and necessary for batch-per-chunk commits and self-invocation scenarios.

---

Share
Use ← → arrow keys to navigate