Spring Programmatic Transaction Management: A Practical Guide for Beginners
Spring's @Transactional annotation is great for declarative transaction management, but it's not always the best choice. In this jotting, you'll learn when and how to use programmatic transaction control in Spring, ideal for situations where you need fine-grained management of transaction lifecycles.
Why You Need Programmatic Transactions?
The Connection Pool Exhaustion Problem
Imagine this common scenario: a method that combines database calls with an external API:
@Transactional
public void processPayment(PaymentRequest request) {
savePaymentRequest(request); // DB
callPaymentProviderApi(request); // External API (slow)
updatePaymentState(request); // DB
saveAuditHistory(request); // DB
}What's the problem here?
When Spring creates the transaction with @Transactional:
- It takes a connection from the pool and keeps it during the ENTIRE method
- The connection remains occupied while waiting for the external API response
- If the API takes 5-10 seconds, that connection is blocked all that time
- Under high load, you exhaust all available connections waiting for slow APIs
Golden rule: Never mix database operations with external API calls within the same transaction.
Solution 1: TransactionTemplate (Recommended)
TransactionTemplate provides a callback-based API for manually managing transactions. It's the cleanest and most modern way to work with programmatic transactions.
Basic Configuration
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.stereotype.Component;
@Component
public class PaymentService {
private final TransactionTemplate transactionTemplate;
public PaymentService(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
}
}Note: Spring Boot automatically configures a
PlatformTransactionManager. You just need to inject it.
Example 1: Transaction with Return Value
public Long createSuccessfulPayment(PaymentRequest request) {
// Execute code within a transaction and return the ID
Long paymentId = transactionTemplate.execute(status -> {
Payment payment = new Payment();
payment.setAmount(request.getAmount());
payment.setReferenceNumber(request.getReference());
payment.setState(Payment.State.SUCCESSFUL);
entityManager.persist(payment);
// ID is auto-generated after persist
return payment.getId();
});
return paymentId;
}What does this do?
- Automatically creates a transaction
- Executes the lambda code inside the transaction
- If everything goes well, automatically commits
- If there's an exception, automatically rolls back
- Returns the value from the lambda
Example 2: Automatic Rollback on Exception
public void createTwoPaymentsWithRollback() {
try {
transactionTemplate.execute(status -> {
Payment first = new Payment();
first.setReferenceNumber("REF-001");
first.setAmount(1000L);
entityManager.persist(first); // OK
Payment second = new Payment();
second.setReferenceNumber("REF-001"); // Duplicate!
second.setAmount(2000L);
entityManager.persist(second); // Throws exception
return null;
});
} catch (Exception e) {
// First payment ALSO gets reverted - guaranteed atomicity
System.out.println("Transaction rolled back: " + e.getMessage());
}
}Example 3: Explicit Manual Rollback
public Long createPaymentWithValidation(PaymentRequest request) {
return transactionTemplate.execute(status -> {
Payment payment = new Payment();
payment.setReferenceNumber(request.getReference());
payment.setAmount(request.getAmount());
entityManager.persist(payment);
// Custom business validation
if (request.getAmount() > 100000) {
// Mark for rollback - transaction will be reverted
status.setRollbackOnly();
return null; // Or throw custom exception
}
return payment.getId();
});
}Example 4: Transaction Without Return Value
public void saveAuditLogEntry(AuditEntry entry) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
auditRepository.save(entry);
// Returns nothing, just executes operations
}
});
}Example 5: Custom Configuration per Instance
You can create multiple TransactionTemplate instances with different configurations:
@Component
public class TransactionConfig {
@Bean
public TransactionTemplate readOnlyTransactionTemplate(PlatformTransactionManager txManager) {
TransactionTemplate template = new TransactionTemplate(txManager);
template.setReadOnly(true); // Optimization for queries
return template;
}
@Bean
public TransactionTemplate serializableTransactionTemplate(PlatformTransactionManager txManager) {
TransactionTemplate template = new TransactionTemplate(txManager);
template.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
template.setTimeout(30); // Seconds
return template;
}
@Bean
public TransactionTemplate requiresNewTransactionTemplate(PlatformTransactionManager txManager) {
TransactionTemplate template = new TransactionTemplate(txManager);
template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
return template;
}
}Available Configurations
| Property | Value | Description |
|---|---|---|
setIsolationLevel() | ISOLATION_READ_UNCOMMITTED | Read uncommitted isolation level |
ISOLATION_READ_COMMITTED | Reads only committed data | |
ISOLATION_REPEATABLE_READ | Prevents non-repeatable reads | |
ISOLATION_SERIALIZABLE | Maximum isolation | |
setPropagationBehavior() | PROPAGATION_REQUIRED | Use existing or create new |
PROPAGATION_REQUIRES_NEW | Always create a new transaction | |
PROPAGATION_NESTED | Nested transaction (savepoint) | |
setTimeout() | Seconds (int) | Maximum execution time |
setReadOnly() | true/false | Optimization for read-only |
Solution 2: PlatformTransactionManager (Low Level)
For total control, use PlatformTransactionManager directly. It's the API used internally by both @Transactional and TransactionTemplate.
Example: Complete Manual Control
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
@Component
public class ManualTransactionService {
private final PlatformTransactionManager transactionManager;
public ManualTransactionService(PlatformTransactionManager txManager) {
this.transactionManager = txManager;
}
public void processWithTotalControl() {
// 1. Define transaction configuration
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
definition.setTimeout(5); // 5 seconds
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 2. Start the transaction
TransactionStatus status = transactionManager.getTransaction(definition);
try {
// 3. Execute business operations
Payment payment = new Payment();
payment.setAmount(500L);
payment.setReferenceNumber("MANUAL-001");
entityManager.persist(payment);
// 4. Commit the transaction
transactionManager.commit(status);
} catch (Exception ex) {
// 5. Rollback on error
transactionManager.rollback(status);
throw new RuntimeException("Error in manual transaction", ex);
}
}
}Comparison: TransactionTemplate vs PlatformTransactionManager
| Aspect | TransactionTemplate | PlatformTransactionManager |
|---|---|---|
| Abstraction level | High (callbacks) | Low (manual) |
| Error handling | Automatic | Manual (try-catch) |
| Automatic rollback | Yes | No (you must call it) |
| Resulting code | Cleaner | More verbose |
| Flexibility | Limited by callback | Total control |
| Recommended use | Most cases | When you need fine control |
Practical Case: Separating DB from External API
Original problem (with exhaustion risk):
@Transactional
public void processOrder(Order order) {
orderRepository.save(order); // DB
shippingApi.createShipment(order); // Slow API
notificationService.notifyCustomer(order); // Slow API
}Solution with TransactionTemplate:
@Component
public class OrderService {
private final TransactionTemplate txTemplate;
private final OrderRepository orderRepository;
private final ShippingApiService shippingApi;
private final NotificationService notificationService;
public void processOrderSafely(Order order) {
// Step 1: Only DB part in transaction
Long orderId = txTemplate.execute(status -> {
order.setStatus("PROCESSING");
orderRepository.save(order);
return order.getId();
});
// Connection is already released!
// Step 2: External API (no connection occupied)
String trackingNumber = shippingApi.createShipment(order);
// Step 3: Another external API (no connection occupied)
notificationService.notifyCustomer(order);
// Step 4: Update final status (new short transaction)
txTemplate.executeWithoutResult(status -> {
Order updated = orderRepository.findById(orderId).orElseThrow();
updated.setTrackingNumber(trackingNumber);
updated.setStatus("SHIPPED");
});
}
}When to Use Each Approach
Use @Transactional (Declarative) when:
- Simple CRUD operations
- No external API calls
- You don't need complex conditional control
- You want to keep code clean and readable
Use TransactionTemplate (Programmatic) when:
- You need to mix DB operations with external I/O
- Conditional logic determines rollback
- Different transaction configurations in the same service
- You need to return values from the transaction
Use PlatformTransactionManager when:
- You need total control over lifecycle
- Multiple transactions in one method
- Integration with non-standard transaction systems
- Detailed transaction logging or auditing
Conclusion
Programmatic transactions give you the control that @Transactional cannot offer. TransactionTemplate is your best ally for most cases where you need to separate database operations from external calls, thus avoiding connection pool exhaustion.
Remember: The goal is not to replace
@Transactional, but to complement it when the constraints of the declarative approach limit your design.
References and Additional Resources
Official Documentation
- Spring Framework - Programmatic Transaction Management: Complete guide from the Spring team on programmatic transaction management. docs.spring.io
- Spring Data Access Documentation: Official documentation on data access and transactions. docs.spring.io
Recommended Articles
- Vlad Mihalcea - Spring Transaction and Connection Management: Deep analysis of how Spring handles database connections and transactions, including lazy connection acquisition optimizations. vladmihalcea.com
- Baeldung - Programmatic Transaction Management in Spring: Practical tutorial with TransactionTemplate and PlatformTransactionManager examples. baeldung.com
- Marco Behler - Spring Transaction Management @Transactional In-Depth: Detailed guide on the internal workings of transactions in Spring. marcobehler.com
Additional Best Practices
- Configure
auto-commit=falsein your connection pool to enable lazy connection acquisition - Set
hibernate.connection.provider_disables_autocommit=truewhen using Hibernate - Consider
DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTIONto maximize connection reuse - Design your service layer so transactional methods are called as late as possible in execution