skip to content

If you call an @Async method from within a @Transactional method, does the async code run inside the same transaction?

level: juniorimportance: must knowfreq 35%

answer

  1. Transaction is thread-bound (ThreadLocal)
  2. @Async = different pool thread
  3. New/independent tx, not caller's
  4. Outer rollback won't undo it
  5. Connection can't be shared across threads

basics

~20 s

No. @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 s

No. 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
java
@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

for a junior

Must know: @Async = different thread = separate transaction; outer rollback does not undo it.

for a middle

Should be able to name ThreadLocal / TransactionSynchronizationManager as the reason.

for a senior

Connects it to entity hand-off, LazyInitializationException, and the AFTER_COMMIT fix.

for a principal

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

context