If you call an @Async method from within a @Transactional method, does the async code run inside the same transaction?
answer
- Transaction is thread-bound (ThreadLocal)
- @Async = different pool thread
- New/independent tx, not caller's
- Outer rollback won't undo it
- Connection can't be shared across threads
basics
~20 sNo. @Async runs the method on a different thread, and Spring ties a transaction to the thread that started it. So the async code runs in its own separate transaction (or none), not the caller's.
solid answer
~40 sNo. Spring's declarative transactions are thread-bound: the JDBC Connection / JPA EntityManager for the active transaction is stored in a ThreadLocal on the thread that opened it. @Async hands the method to a different thread from a TaskExecutor pool, and that thread has none of the caller's thread-local transaction state. So the async method starts fresh — if it (or a method it calls) is @Transactional it opens a brand-new independent transaction; otherwise it runs with no transaction. The practical consequences: the async work commits or rolls back on its own timeline, it is not rolled back if the outer method fails, and it cannot see the caller's still-uncommitted changes. Treat the async boundary as a hard transaction boundary.
code
java · 22 lines@Service
public class OrderService {
@Transactional
public void placeOrder(Order order) {
orderRepo.save(order); // tx A, on THIS thread
auditService.recordAsync(); // hops to a pool thread -> tx B (or none)
throw new RuntimeException("boom"); // rolls back tx A only
}
}
@Service
public class AuditService {
@Async
@Transactional
public void recordAsync() {
// Runs on a TaskExecutor thread; no thread-bound tx inherited,
// so this opens a fresh, independent transaction that commits
// regardless of the "boom" above.
auditRepo.save(new AuditEntry("order placed"));
}
}go deeper
Must know: @Async = different thread = separate transaction; outer rollback does not undo it.
Should be able to name ThreadLocal / TransactionSynchronizationManager as the reason.
Connects it to entity hand-off, LazyInitializationException, and the AFTER_COMMIT fix.
Frames it as a deliberate design (one connection per tx) and knows the reliable-delivery patterns.
## The core idea Spring's `@Transactional` is **thread-bound**. When a transaction starts, Spring stores the transactional resources — the JDBC `Connection` (or JPA `EntityManager`/`Session`) — in a `ThreadLocal` managed by `TransactionSynchronizationManager`. Every repository/`JdbcTemplate`/`EntityManager` call on that same thread looks up that thread-local resource, which is how they all join the one transaction. `@Async` (enabled by `@EnableAsync`) makes Spring's proxy submit the method to a `TaskExecutor`, so it runs on a **different thread** from a pool. `ThreadLocal` values are per-thread and are **not** copied to that pool thread. Therefore the async method sees an empty transaction context. ## What actually happens on the async thread - If the async method (or something it calls through a Spring proxy) is `@Transactional` with the default `Propagation.REQUIRED`, since there is *no* existing transaction on this thread, a **new** transaction is opened. - If nothing on the async path is transactional, the DB work runs **non-transactionally** (auto-commit per statement). - Either way it is **independent** of the caller's transaction. ## Consequences to remember 1. **No shared rollback.** If the outer `@Transactional` method throws and rolls back, the async insert has already committed (or will) and stays in the database. 2. **No dirty reads across the boundary.** The async thread runs in a different transaction, so with normal isolation it cannot read rows the caller has written but not yet committed. 3. **Detached entities / `LazyInitializationException`.** If you pass a JPA entity to the async method, the persistence context that owned it lives on the caller's thread and may already be closed; touching a lazy association on the async thread throws `LazyInitializationException`. ## Why it's designed this way A transaction ≈ one physical DB connection. Two threads cannot safely share one connection concurrently, so Spring deliberately does not propagate the transaction across threads. This is the same reason a manually spawned `new Thread(...)`/`ExecutorService` task also loses the transaction. ## The fix in one line Do not rely on the outer transaction covering async work. Kick off the async work **after** the transaction commits (e.g. `@TransactionalEventListener(phase = AFTER_COMMIT)`), and pass **IDs, not entities**, re-fetching inside the async thread's own transaction.
- Does a manually created `new Thread(...)` behave differently from @Async here?No. Both run on a thread that doesn't carry the caller's thread-local transaction, so both lose the transaction context the same way. @Async just uses a managed TaskExecutor pool instead of a raw thread.
- If the async method has no @Transactional at all, what transaction does its DB write use?None — it runs non-transactionally (statement-level auto-commit). It still isn't part of the caller's transaction.
saying these in an interview costs you the question
- Thinking @Async 'inherits' the caller's transaction
- Assuming an outer rollback will undo the async insert
- Believing the async thread can read the caller's uncommitted rows