A practical guide to handling transactions correctly in async batch operations with database updates and remote service calls.
📌 Introduction
In many enterprise systems, we often deal with batch operations executed in parallel threads, such as:
- Importing records and updating the database
- Sending notifications after saving data
- Interacting with external APIs after writing business-critical information to a local DB
In such cases, one key concern is:
How do we ensure each thread runs inside a proper transaction without affecting others?
This article shows how to design each thread to handle its own transaction independently, ensuring that failures in one thread do not impact the rest.
🔍 Problem Statement
Imagine this logic:
- Update database (local transaction)
- Call remote service (external dependency)
We want the transaction behavior to be:
- If any step fails, rollback only the current thread’s transaction
- Other threads should not be affected
- We must not accidentally share a single transaction context across threads
Incorrect Approach: Async Inside Transactional Method
@Service public class BatchService { @Transactional public void processBatch(List<Item> items) { items.forEach(item -> { // Bad: Spawning async work inside a transactional method CompletableFuture.runAsync(() -> { updateDb(item); // Won't participate in the outer transaction callRemoteService(item); }); }); } }
❌ This doesn't work as expected:
@Transactional
does not propagate into a new thread.
The DB update may execute outside of the transaction context!
✅ Correct Design: Transaction Per Thread
Use an explicitly scoped transactional method called inside each thread, not around it.
@Service public class BatchService { @Autowired private ItemService itemService; public void processBatch(List<Item> items) { items.forEach(item -> { CompletableFuture.runAsync(() -> { try { itemService.processOneItem(item); // Each thread has its own transaction } catch (Exception e) { // Log and continue; only this thread's TX rolls back } }); }); } } @Service public class ItemService { @Transactional public void processOneItem(Item item) { updateDb(item); // part of transaction callRemoteService(item); // if this fails, TX rolls back } }
Now each thread runs
@Transactional
logic inprocessOneItem
, and any failure in DB or remote call will trigger rollback for only that item.
⚠️ Bonus Tips
- Don't use
@Transactional
on private methods (Spring AOP won't proxy them) - If you're using thread pools (
ExecutorService
), make sure each task independently calls a public service method annotated with@Transactional
- Log failures but don't let them crash the whole batch
Alternative: Using TransactionTemplate in the Same Class
In some cases, you may want to keep the logic in the same class (e.g., no split between BatchService
and ItemService
). Since @Transactional
won’t work properly in self-invocation
(Spring AOP won't trigger), you can use TransactionTemplate
programmatically:
@Service public class BatchService { @Autowired private PlatformTransactionManager transactionManager; public void processBatch(List<Item> items) { TransactionTemplate template = new TransactionTemplate(transactionManager); items.forEach(item -> { CompletableFuture.runAsync(() -> { try { template.execute(status -> { updateDb(item); callRemoteService(item); // any exception rolls back return null; }); } catch (Exception e) { // log and continue } }); }); } private void updateDb(Item item) { // update database } private void callRemoteService(Item item) { // call external API } }
🧩 This approach avoids AOP limitations and gives you full control over each thread’s transaction boundary.
💬 Final Thoughts
Proper transaction management in asynchronous batch operations can prevent data corruption, increase resilience, and make debugging easier.
If you're designing a system with concurrent operations and transactional safety, follow this "transaction-per-thread" model.
🔗 Related Reading
- Spring Docs: @Transactional and Thread Boundaries
- Java Concurrency in Practice – Effective parallel execution models
- How Spring AOP works under the hood
Top comments (1)
Nice post.