Home Java Mastering Programmatic Transactions in Spring: TransactionTemplate & PlatformTransactionManager
Advanced 7 min · July 14, 2026

Mastering Programmatic Transactions in Spring: TransactionTemplate & PlatformTransactionManager

Go beyond @Transactional.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Spring Boot 3.x or Spring Framework 6.x project
  • Basic understanding of Spring's @Transactional annotation
  • Familiarity with dependency injection and Spring configuration
  • A running database (H2 for testing, PostgreSQL/MySQL for production)
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use TransactionTemplate for most programmatic transaction needs; it's simpler and less error-prone.
  • Fall back to PlatformTransactionManager directly only when you need to manage transactions across multiple threads or non-standard scopes.
  • Always set explicit rollback rules; never rely on defaults for checked exceptions.
  • Watch out for transaction isolation levels and propagation behaviors; they can silently corrupt data if misconfigured.
  • Test transaction rollback behavior with actual database calls, not mocks.
✦ Definition~90s read
What is Programmatic Transaction Management in Spring?

Programmatic transactions in Spring are a way to manage database transaction boundaries explicitly using TransactionTemplate or PlatformTransactionManager, giving you fine-grained control over commit, rollback, propagation, and isolation without relying on declarative annotations.

Think of a transaction like a bank transfer: you want to take money from one account and add it to another as a single, all-or-nothing operation.
Plain-English First

Think of a transaction like a bank transfer: you want to take money from one account and add it to another as a single, all-or-nothing operation. @Transactional is like an automatic teller that handles the whole thing for you. But sometimes you need to manually control each step — like when you're moving money between multiple banks and need to decide on the spot whether to commit or roll back. TransactionTemplate is like a programmable teller that follows your script, while PlatformTransactionManager is the vault itself — you can open, close, and manage the vault directly if you need to.

Most Spring developers start with @Transactional. It's clean, declarative, and works 90% of the time. But that 10% — when you need to dynamically decide to commit or rollback based on runtime conditions, or when you're dealing with multiple data sources, or when you need to manage transaction boundaries across async operations — that's where @Transactional falls apart. I've seen teams spend days debugging transaction issues that could have been solved in minutes with programmatic transaction management.

Enter TransactionTemplate and PlatformTransactionManager. These are the tools Spring provides for fine-grained, programmatic transaction control. TransactionTemplate is a callback-based template that handles transaction lifecycle for you. PlatformTransactionManager is the lower-level SPI that actually manages transactions against a specific resource (like a JDBC DataSource or JPA EntityManagerFactory).

In this article, I'll show you when and how to use each, with real code examples from payment processing systems, batch jobs, and multi-tenant applications. We'll also look at a production incident where a naive @Transactional caused a data corruption that cost a fintech startup thousands. By the end, you'll know exactly when to ditch the annotation and take manual control.

Why Go Programmatic? The Case Against @Transactional

Let's be honest: @Transactional is great for simple CRUD operations. But in real-world enterprise applications, you often need more control. Here's when I ditch the annotation:

  1. Dynamic Rollback Decisions: You need to commit or rollback based on a business rule, not just an exception. For example, in a payment reconciliation system, you might want to rollback a transaction if the external payment gateway returns a specific error code, but commit if it's a temporary network glitch.
  2. Multiple Transaction Boundaries in One Method: You have a loop that processes many items, and each item should be its own transaction. @Transactional on the method would wrap the entire loop in one transaction, which is almost never what you want.
  3. Non-Standard Transaction Propagation: You need to suspend a transaction, run some code without a transaction, and then resume. Or you need to run multiple transactions sequentially within the same method.
  4. Testing and Mocking: When writing integration tests, you often need to manually control transaction boundaries without relying on Spring's AOP proxies.

I once worked on a SaaS billing system where we processed thousands of subscription renewals every hour. Each renewal involved charging a credit card, updating the database, and sending a notification. If any step failed, we wanted to rollback only that renewal, not the entire batch. @Transactional on the batch method would have been disastrous. We used TransactionTemplate inside a loop, and it worked flawlessly.

Here's the hard truth: if you're using @Transactional on a method that contains a loop, a conditional, or a try-catch that swallows exceptions, you're probably doing it wrong. Programmatic transactions give you the control you need without the magic.

ProgrammaticVsDeclarative.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Declarative approach - BAD for loops
@Service
public class BatchService {
    @Transactional
    public void processBatch(List<Item> items) {
        for (Item item : items) {
            processItem(item); // If this throws, ALL items rollback
        }
    }
}

// Programmatic approach - GOOD
@Service
public class BatchService {
    private final TransactionTemplate transactionTemplate;

    public BatchService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
        this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        this.transactionTemplate.setTimeout(30);
    }

    public void processBatch(List<Item> items) {
        for (Item item : items) {
            transactionTemplate.execute(status -> {
                try {
                    processItem(item);
                    return null;
                } catch (Exception e) {
                    status.setRollbackOnly();
                    throw e; // rethrow to trigger rollback
                }
            });
        }
    }
}
⚠ Common Pitfall: Swallowing Exceptions
📊 Production Insight
In production, always set an explicit timeout on TransactionTemplate to prevent long-running transactions from holding database locks. I've seen a single misbehaving transaction bring down an entire PostgreSQL cluster.
🎯 Key Takeaway
Use TransactionTemplate when you need per-item transaction boundaries inside loops or conditional logic.

TransactionTemplate: The Workhorse of Programmatic Transactions

TransactionTemplate is my go-to for programmatic transaction management. It's part of the Spring transaction abstraction and provides a simple callback-based API. Here's how it works:

You create a TransactionTemplate with a PlatformTransactionManager (usually injected via constructor). Then you call execute() with a TransactionCallback that contains your business logic. The template handles begin, commit, and rollback for you.

Key configuration options
  • Propagation behavior: Default is REQUIRED. Use REQUIRES_NEW to suspend any existing transaction and start a new one.
  • Isolation level: Default is the database's default. Set it explicitly if you need repeatable reads or serializable.
  • Timeout: Default is -1 (no timeout). Always set a timeout in production to avoid indefinite locks.
  • Rollback rules: By default, only unchecked exceptions trigger rollback. Use setRollbackRules() to specify checked exceptions that should cause rollback.

Here's a real example from a payment processing service. We need to charge a customer's card, update the subscription, and log the transaction. If any step fails, we want to rollback everything:

```java @Service public class PaymentService { private final TransactionTemplate txTemplate;

public PaymentService(PlatformTransactionManager txManager) { this.txTemplate = new TransactionTemplate(txManager); this.txTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); this.txTemplate.setTimeout(10); // Rollback on checked PaymentException too this.txTemplate.setRollbackOnCommitFailure(true); }

public PaymentResult chargeCustomer(Customer customer, BigDecimal amount) { return txTemplate.execute(status -> { try { // 1. Charge credit card via external gateway String chargeId = paymentGateway.charge(customer.getCardToken(), amount); // 2. Update subscription in database subscriptionService.renew(customer.getId(), amount); // 3. Log transaction transactionLogService.log(customer.getId(), chargeId, amount); return new PaymentResult(true, chargeId); } catch (PaymentGatewayException e) { // Business decision: rollback for certain errors if (e.isRetryable()) { status.setRollbackOnly(); throw e; // rethrow to trigger rollback } else { // Non-retryable: still rollback but log differently status.setRollbackOnly(); throw new NonRetryablePaymentException(e); } } }); } } ```

Notice how we have full control over when to rollback. With @Transactional, we'd have to wrap everything in a try-catch and rethrow a runtime exception, which is less expressive.

PaymentService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class PaymentService {
    private final TransactionTemplate txTemplate;

    public PaymentService(PlatformTransactionManager txManager) {
        this.txTemplate = new TransactionTemplate(txManager);
        this.txTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        this.txTemplate.setTimeout(10);
    }

    public PaymentResult chargeCustomer(Customer customer, BigDecimal amount) {
        return txTemplate.execute(status -> {
            try {
                String chargeId = paymentGateway.charge(customer.getCardToken(), amount);
                subscriptionService.renew(customer.getId(), amount);
                transactionLogService.log(customer.getId(), chargeId, amount);
                return new PaymentResult(true, chargeId);
            } catch (PaymentGatewayException e) {
                status.setRollbackOnly();
                throw e;
            }
        });
    }
}
💡Always Set Timeout
📊 Production Insight
I once had a transaction that worked fine in development but timed out in production because the database was under load. The timeout saved us from a complete outage. Without it, the transaction would have held locks indefinitely, causing a deadlock cascade.
🎯 Key Takeaway
TransactionTemplate provides a clean, callback-based API for programmatic transactions with full control over propagation, isolation, and rollback behavior.

PlatformTransactionManager: When You Need Full Control

TransactionTemplate is great for most cases, but sometimes you need to go lower level. PlatformTransactionManager is the SPI that manages transactions directly. You call getTransaction(), then commit() or rollback().

When would you need this? - Multiple threads: If you need to share a transaction across threads (e.g., fork-join), you can't use TransactionTemplate because it's tied to a single thread. You'll need to get the transaction object and pass it around. - Non-standard transaction lifecycle: For example, you want to begin a transaction, do some work, then conditionally commit or rollback based on an external event. - Custom resource management: If you're integrating with a non-Spring resource (e.g., a legacy JDBC connection), you might need to manage the transaction manually.

Here's an example of manual transaction management for a batch job that processes items in parallel:

```java @Service public class ParallelBatchProcessor { private final PlatformTransactionManager txManager; private final TransactionDefinition txDef;

public ParallelBatchProcessor(PlatformTransactionManager txManager) { this.txManager = txManager; this.txDef = new DefaultTransactionDefinition(); ((DefaultTransactionDefinition) this.txDef).setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); ((DefaultTransactionDefinition) this.txDef).setTimeout(30); }

public void processInParallel(List<Item> items) { List<CompletableFuture<Void>> futures = items.stream() .map(item -> CompletableFuture.runAsync(() -> processItemInTransaction(item))) .collect(Collectors.toList()); CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); }

private void processItemInTransaction(Item item) { TransactionStatus status = txManager.getTransaction(txDef); try { // Business logic processItem(item); txManager.commit(status); } catch (Exception e) { txManager.rollback(status); throw e; // rethrow after rollback } } } ```

Notice that each thread gets its own transaction. This is impossible with @Transactional because the annotation creates a transaction bound to the calling thread. With PlatformTransactionManager, you have full control.

However, with great power comes great responsibility. You must ensure that every code path calls either commit() or rollback(). Forgetting to do so will leave the transaction hanging, potentially locking resources indefinitely.

ParallelBatchProcessor.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Service
public class ParallelBatchProcessor {
    private final PlatformTransactionManager txManager;
    private final TransactionDefinition txDef;

    public ParallelBatchProcessor(PlatformTransactionManager txManager) {
        this.txManager = txManager;
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        def.setTimeout(30);
        this.txDef = def;
    }

    public void processInParallel(List<Item> items) {
        List<CompletableFuture<Void>> futures = items.stream()
            .map(item -> CompletableFuture.runAsync(() -> processItemInTransaction(item)))
            .collect(Collectors.toList());
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
    }

    private void processItemInTransaction(Item item) {
        TransactionStatus status = txManager.getTransaction(txDef);
        try {
            processItem(item);
            txManager.commit(status);
        } catch (Exception e) {
            txManager.rollback(status);
            throw e;
        }
    }
}
⚠ Don't Forget to Commit or Rollback
📊 Production Insight
In a high-throughput system, a single uncommitted transaction can block the connection pool and bring down the application. Always use monitoring to detect long-running transactions.
🎯 Key Takeaway
Use PlatformTransactionManager directly when you need to manage transactions across threads or have non-standard lifecycle requirements. Always ensure proper cleanup.

What the Official Docs Won't Tell You

I've been using Spring's programmatic transaction support for over a decade. Here are the gotchas that the official documentation glosses over:

1. TransactionTemplate and Thread Safety TransactionTemplate is thread-safe after configuration. However, if you change its properties (e.g., timeout) after it's been used, you're asking for trouble. Always configure it once at construction time and treat it as immutable. I've seen teams create a new TransactionTemplate for every request, which defeats the purpose of reuse and can cause memory leaks.

2. PlatformTransactionManager.getTransaction() Does Not Always Create a New Transaction This is a huge one. The behavior depends on the propagation setting. If you call getTransaction() with PROPAGATION_REQUIRED inside an existing transaction, it will join that transaction. If you want a new transaction, you must use PROPAGATION_REQUIRES_NEW. Many developers assume getTransaction() always starts a new transaction, leading to unexpected sharing.

3. Checked Exceptions and Rollback By default, both @Transactional and TransactionTemplate only rollback for unchecked exceptions (RuntimeException and Error). Checked exceptions do NOT trigger rollback. This is a common source of data corruption. Always set explicit rollback rules for checked exceptions using rollbackFor or setRollbackRules().

4. TransactionTemplate.execute() Does Not Propagate Checked Exceptions The TransactionCallback interface's doInTransaction() method throws Exception, but TransactionTemplate.execute() only propagates unchecked exceptions. If your callback throws a checked exception, it gets wrapped in an UndeclaredThrowableException. To avoid this, catch checked exceptions inside the callback and either convert them to unchecked exceptions or handle them appropriately.

5. Nested Transactions and Savepoints Spring supports savepoints for nested transactions if your underlying resource (e.g., JDBC) supports them. But beware: savepoints are only available with PROPAGATION_NESTED, and they rely on the JDBC driver's support. Not all databases support savepoints (e.g., some older MySQL versions). Test thoroughly.

6. Transaction Synchronization If you register transaction synchronization callbacks (e.g., via TransactionSynchronizationManager), they run after commit or rollback. But if you're using PlatformTransactionManager directly, you must ensure you call triggerAfterCompletion() manually. TransactionTemplate does this automatically, but raw usage does not.

CheckedExceptionRollback.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
// BAD: Checked exception won't trigger rollback by default
transactionTemplate.execute(status -> {
    throw new BusinessException("Checked exception"); // No rollback!
});

// GOOD: Explicit rollback rules
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setRollbackRules(Collections.singletonList(new RollbackRuleAttribute(BusinessException.class)));
TransactionTemplate template = new TransactionTemplate(txManager, def);
template.execute(status -> {
    throw new BusinessException("Checked exception"); // Now rolls back
});
🔥Rollback Rules Are Your Friend
📊 Production Insight
At a previous job, we had a critical bug where a checked exception was thrown inside a transaction, but the transaction committed anyway. The data inconsistency took weeks to detect and months to clean up. We now have a team rule: any checked exception inside a transaction must be explicitly configured for rollback.
🎯 Key Takeaway
The docs don't emphasize enough the pitfalls with checked exceptions, thread safety of TransactionTemplate, and the non-intuitive behavior of getTransaction().

Transaction Propagation: The Silent Killer

Transaction propagation determines how transactions behave when called within an existing transaction. Spring supports seven propagation behaviors, but most developers only use REQUIRED and REQUIRES_NEW. Here's what you need to know:

  • REQUIRED (default): Joins the current transaction if one exists; creates a new one if not. This is what @Transactional uses by default.
  • REQUIRES_NEW: Suspends the current transaction (if any) and creates a new one. The suspended transaction resumes after the new one completes.
  • NESTED: Uses a savepoint within the current transaction. If the inner transaction rolls back, only the savepoint is rolled back, not the entire outer transaction. This is tricky and database-dependent.
  • MANDATORY: Throws an exception if no current transaction exists.
  • NEVER: Throws an exception if a current transaction exists.
  • SUPPORTS: If a transaction exists, joins it; otherwise, runs without a transaction.
  • NOT_SUPPORTED: Suspends any current transaction and runs without one.

The most common mistake is using REQUIRED when you should use REQUIRES_NEW. For example, in an audit logging service, you typically want each log entry to be persisted independently, regardless of the caller's transaction. If you use REQUIRED, the log entry will be part of the caller's transaction and will rollback if the caller rolls back. That's usually not what you want.

Here's a real production scenario: We had a service that processed orders. Each order processing involved updating inventory, charging the customer, and sending a confirmation email. The email sending was done in a separate transaction because we didn't want a failed email to rollback the order. We used REQUIRES_NEW for the email transaction. But the developer mistakenly used REQUIRED, and when the email service was down, the entire order rolled back, causing lost revenue. The fix was simple: change propagation to REQUIRES_NEW.

PropagationExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Service
public class OrderService {
    private final TransactionTemplate orderTx;
    private final TransactionTemplate emailTx;

    public OrderService(PlatformTransactionManager txManager) {
        DefaultTransactionDefinition orderDef = new DefaultTransactionDefinition();
        orderDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        this.orderTx = new TransactionTemplate(txManager, orderDef);

        DefaultTransactionDefinition emailDef = new DefaultTransactionDefinition();
        emailDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        this.emailTx = new TransactionTemplate(txManager, emailDef);
    }

    public void placeOrder(Order order) {
        orderTx.execute(status -> {
            // Update inventory, charge customer
            inventoryService.deduct(order);
            paymentService.charge(order);
            // Send confirmation in its own transaction
            emailTx.execute(emailStatus -> {
                emailService.sendConfirmation(order);
                return null;
            });
            return null;
        });
    }
}
💡Use REQUIRES_NEW for Independent Operations
📊 Production Insight
I've seen production incidents where a single failing email notification rolled back a critical order transaction because of incorrect propagation. Always audit your transaction boundaries.
🎯 Key Takeaway
Choose propagation behavior carefully. REQUIRED is not always the right choice; use REQUIRES_NEW to isolate independent operations.

Testing Programmatic Transactions: Don't Skip This

Testing transactions is notoriously tricky. With @Transactional, Spring's test support can automatically rollback after each test. But with programmatic transactions, you're on your own. Here's how I approach testing:

Unit Testing TransactionTemplate You can mock PlatformTransactionManager and verify that commit/rollback are called correctly. However, this doesn't test the actual transaction behavior. For that, you need integration tests.

Integration Testing Use an in-memory database (H2) or a lightweight test container (Testcontainers). Create a real PlatformTransactionManager bean and inject it into your service. Then, write tests that actually perform database operations and verify the state after commit or rollback.

Key things to test
  • Transaction commits when no exception is thrown.
  • Transaction rolls back on unchecked exceptions.
  • Transaction rolls back on checked exceptions if configured.
  • Transaction timeout triggers rollback.
  • Nested transactions and savepoints work correctly.

Here's an example using Spring Boot test:

```java @SpringBootTest class PaymentServiceTest { @Autowired private PaymentService paymentService; @Autowired private JdbcTemplate jdbcTemplate;

@Test void shouldRollbackOnPaymentException() { assertThrows(PaymentGatewayException.class, () -> { paymentService.chargeCustomer(new Customer("test"), BigDecimal.TEN); }); // Verify no data was persisted Integer count = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM transactions WHERE customer_id = ?", Integer.class, "test"); assertEquals(0, count); } } ```

Notice we use JdbcTemplate to directly query the database and verify the state. This is far more reliable than relying on mocks.

PaymentServiceTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@SpringBootTest
class PaymentServiceTest {
    @Autowired
    private PaymentService paymentService;
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    void shouldRollbackOnPaymentException() {
        assertThrows(PaymentGatewayException.class, () -> {
            paymentService.chargeCustomer(new Customer("test"), BigDecimal.TEN);
        });
        Integer count = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM transactions WHERE customer_id = ?", Integer.class, "test");
        assertEquals(0, count);
    }

    @Test
    void shouldCommitOnSuccess() {
        PaymentResult result = paymentService.chargeCustomer(new Customer("success"), BigDecimal.TEN);
        assertTrue(result.isSuccess());
        Integer count = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM transactions WHERE customer_id = ?", Integer.class, "success");
        assertEquals(1, count);
    }
}
🔥Don't Rely Solely on Mocks
📊 Production Insight
In one project, our unit tests passed but integration tests failed because the H2 database behaved differently than PostgreSQL. We switched to Testcontainers with a real PostgreSQL image, which caught several transaction isolation issues early.
🎯 Key Takeaway
Test programmatic transactions with integration tests that use a real database and verify data state after commit/rollback.
● Production incidentPOST-MORTEMseverity: high

The Midnight Transaction That Lost Invoices

Symptom
Every night, the batch job generated invoices for about 50% of eligible customers. No errors, no logs. Just missing invoices.
Assumption
The developer assumed @Transactional on the batch method would wrap each customer's processing in its own transaction, rolling back on failure.
Root cause
@Transactional on the batch method created a single transaction for the entire loop. When a single customer's processing failed (due to a transient deadlock), the whole transaction rolled back, including successfully processed customers. But the rollback didn't re-throw the exception — it was caught and logged inside the loop. So the transaction committed partially, leaving some customers processed and others not, with no clear audit trail.
Fix
Replaced @Transactional with TransactionTemplate inside the loop, so each customer iteration had its own transaction. Also added explicit rollback rules for the checked exception that was being swallowed.
Key lesson
  • @Transactional on a method that loops over operations is a red flag — each iteration should typically be its own transaction.
  • Never swallow exceptions inside a transaction boundary unless you absolutely know what you're doing. Log and rethrow, or use a savepoint.
  • Always test transaction rollback behavior with realistic data volumes and concurrent access patterns.
  • Use TransactionTemplate when you need fine-grained control over transaction boundaries inside loops or conditional logic.
  • Monitor transaction metrics (commit/rollback counts) in production to catch silent rollbacks early.
Production debug guideSymptom to Action4 entries
Symptom · 01
Transaction not rolling back on exception
Fix
Check if the exception is checked and not configured for rollback. Verify PlatformTransactionManager's rollback rules. Use TransactionTemplate with explicit rollbackFor.
Symptom · 02
Transaction commits but data is inconsistent
Fix
Look for swallowed exceptions inside transaction boundaries. Ensure each logical unit of work has its own transaction. Check for unintended transaction propagation (e.g., REQUIRES_NEW vs REQUIRED).
Symptom · 03
Transaction timeout errors in production
Fix
Verify timeout settings on TransactionTemplate or @Transactional. Check for long-running operations (e.g., HTTP calls) inside transactions. Move I/O outside transaction boundaries.
Symptom · 04
LazyInitializationException after transaction commit
Fix
Ensure you access lazy-loaded entities within the transaction scope. Use fetch joins or EntityGraphs. Consider Open Session in View only as a last resort.
★ Quick Debug Cheat SheetQuick reference for common transaction issues
Transaction not rolling back on checked exception
Immediate action
Add rollbackFor = {SpecificException.class} to @Transactional or TransactionTemplate
Commands
Enable DEBUG logging for org.springframework.transaction
Check logs for 'Completing transaction' with commit or rollback
Fix now
Use TransactionTemplate with explicit rollbackFor
Multiple data sources - transaction not spanning+
Immediate action
Use ChainedTransactionManager or separate transaction managers
Commands
Check which PlatformTransactionManager bean is used
Verify @Transactional('transactionManagerName') or TransactionTemplate constructor
Fix now
Implement distributed transaction with JTA or use compensating transactions
Transaction timeout exceeded+
Immediate action
Reduce timeout or optimize the transaction code
Commands
Check timeout setting on TransactionTemplate.setTimeout()
Profile database query times
Fix now
Move long-running external calls outside transaction
Feature@TransactionalTransactionTemplatePlatformTransactionManager
Control LevelDeclarative (AOP)Programmatic (callback)Programmatic (manual)
Ease of UseHighMediumLow
Thread SafetyPer proxyThread-safe after configNot thread-safe by default
Rollback RulesVia rollbackForVia setRollbackRules()Manual in code
Best ForSimple CRUDLoops, conditional rollbackMulti-threaded, custom lifecycle
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
ProgrammaticVsDeclarative.java@ServiceWhy Go Programmatic? The Case Against @Transactional
PaymentService.java@ServiceTransactionTemplate
ParallelBatchProcessor.java@ServicePlatformTransactionManager
CheckedExceptionRollback.javatransactionTemplate.execute(status -> {What the Official Docs Won't Tell You
PropagationExample.java@ServiceTransaction Propagation
PaymentServiceTest.java@SpringBootTestTesting Programmatic Transactions

Key takeaways

1
Use TransactionTemplate for most programmatic transaction needs; it's simpler and less error-prone than using PlatformTransactionManager directly.
2
Always set explicit rollback rules for checked exceptions and configure timeouts to prevent long-running transactions.
3
Test programmatic transactions with integration tests using a real database to verify commit and rollback behavior.
4
Be cautious with transaction propagation
use REQUIRES_NEW to isolate independent operations and avoid unintended transaction sharing.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the difference between TransactionTemplate and @Transactional. W...
Q02SENIOR
How does Spring's transaction propagation work? Give an example where RE...
Q03SENIOR
What are the common pitfalls when using PlatformTransactionManager direc...
Q01 of 03SENIOR

Explain the difference between TransactionTemplate and @Transactional. When would you choose one over the other?

ANSWER
@Transactional is declarative and works via AOP proxies. It's simpler but limited: you can't easily change transaction behavior at runtime, and it's problematic inside loops or with multiple threads. TransactionTemplate is programmatic and gives you full control over transaction boundaries, rollback decisions, and propagation. I'd choose TransactionTemplate when I need per-item transactions in a loop, conditional rollback, or transaction management across threads.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What's the difference between TransactionTemplate and PlatformTransactionManager?
02
When should I use programmatic transactions instead of @Transactional?
03
How do I handle checked exceptions with TransactionTemplate?
04
Can I use TransactionTemplate with multiple data sources?
05
Is TransactionTemplate thread-safe?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Hibernate & JPA. Mark it forged?

7 min read · try the examples if you haven't

Previous
Using a List of Values in a JdbcTemplate IN Clause
13 / 28 · Hibernate & JPA
Next
Using Read-Only Transactions in Spring: Performance and Optimization