DEV Community

Thellu
Thellu

Posted on

How to Manage Transactions in Asynchronous Threads in Java (Spring)

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:

  1. Update database (local transaction)
  2. 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); }); }); } } 
Enter fullscreen mode Exit fullscreen mode

❌ 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 } } 
Enter fullscreen mode Exit fullscreen mode

Now each thread runs @Transactional logic in processOneItem, 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 } } 
Enter fullscreen mode Exit fullscreen mode

🧩 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

Top comments (1)

Collapse
 
michael_liang_0208 profile image
Michael Liang

Nice post.