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.