Understanding the Compatibility of @Transactional and @Async in Spring
This tutorial explains how Spring's @Transactional and @Async annotations work, explores their interaction in various scenarios such as bank‑transfer examples, discusses thread‑context propagation, highlights pitfalls when mixing them, and provides best‑practice recommendations for maintaining data integrity.
Hello everyone, I am Chen~
The annotations @Transactional and @Async are powerful tools that developers use frequently. However, when they meet, can they coexist harmoniously and deliver maximum efficiency?
Many developers have never deeply considered this question. In this article we will explore the compatibility between @Transactional and @Async in the Spring framework.
Deep Understanding of @Transactional and @Async
The @Transactional annotation acts like a strict steward that creates an atomic code block. All operations inside this block are treated as a single unit; if any operation throws an exception, the whole block is rolled back, ensuring data consistency.
The @Async annotation behaves like a sprinter, telling Spring that the annotated method or class can run in parallel with the calling thread. When an @Async method is invoked, Spring starts its execution in a separate thread with a different context, which can greatly improve performance for time‑consuming tasks.
In complex business scenarios we often want both data consistency and high performance. Spring does allow us to combine @Transactional and @Async , but we must use them carefully.
Can @Transactional and @Async be used together?
1. Build a sample application: bank transfer
To illustrate transaction and asynchronous code, we use a simple bank‑transfer function. The transfer consists of withdrawing money from one account and depositing it into another, which translates to updating the corresponding database records.
Implementation steps:
Use findById() to locate an account by its ID. If the ID is not found, throw IllegalArgumentException .
Update the retrieved account with the new balance.
Persist the changes with CrudRepository.save() .
Potential risk points include:
Target account not found, causing an exception.
The save() operation succeeds for the source account but fails for the destination account, leading to a partial failure.
Without a transaction, such partial failures can cause data‑consistency problems, e.g., money is deducted from the source account but never credited to the destination account.
2. Calling @Transactional from an @Async method
When an @Async method invokes a @Transactional method, Spring correctly propagates the transaction context, ensuring data consistency.
Example:
public class TransferService {
@Async
public void transferAsync() {
transfer(); // @Transactional method
}
@Transactional
public void transfer() {
// database operations that will be rolled back on failure
}
}Spring propagates the thread context from transferAsync() to transfer() , so no data is lost during the interaction. Only the code inside transfer() participates in the transaction; code outside remains isolated.
3. Calling an @Async method from a @Transactional method
Spring uses ThreadLocal to manage the current thread's transaction, which means the transaction context is not shared across different threads. Therefore, when a @Transactional method calls an @Async method, the async method runs without the transaction context.
Consider adding an async printReceipt() call inside a transactional transfer() method. Because printReceipt() runs in a separate thread, it may read or write data before the transaction is committed, leading to unpredictable behavior such as printing a receipt for a transfer that has not yet been persisted.
To avoid this inconsistency, do not invoke @Async methods from within a @Transactional method.
4. Using @Transactional at the class level
Annotating a class with @Transactional makes all its public methods transactional. If any of those methods are also annotated with @Async , the same issues described above arise: the async method runs in a different thread without the transaction context, and failures outside the transactional method will not be rolled back.
For example, a method transferAsync() that is both @Transactional and @Async defines a transaction unit that executes on a different thread. The code inside the method will roll back on failure, but code outside the method (or code called after the async boundary) will not be part of the rollback.
Mixing transactional and asynchronous behavior can make debugging difficult because the expected rollback semantics may be broken when an async method is involved.
Summary
From a data‑integrity perspective, you can safely use @Transactional and @Async together when an @Async method calls a @Transactional method, because Spring propagates the same context. However, calling an @Async method from within a @Transactional method can lead to data‑integrity problems, as the async execution does not inherit the transaction context.
Promotional notice: Chen has released a new booklet titled “Enterprise‑Level Practical Summary – 40 Lectures”, covering core backend issues such as JVM, databases, performance tuning, etc. Original price 99 CNY, now discounted to 11.9 CNY for a permanent license. Scan the QR code below to subscribe.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.