skip to content

What happens if you perform JPA/JDBC writes inside an afterCommit callback? Why do people say afterCommit runs 'outside' the transaction, and how do you write to the DB safely there?

level: seniorimportance: should knowfreq 34%

answer

  1. afterCommit = resources already unbound
  2. direct write → no active tx / auto-commit / throws
  3. delegate to REQUIRES_NEW bean method
  4. self-call bypasses proxy = no new tx
  5. dual-write gap → outbox / idempotency

basics

~20 s

By afterCommit the original transaction is already committed and its resources are being cleaned up, so writes there are not part of that transaction. To write safely, open a fresh transaction (e.g. a REQUIRES_NEW @Transactional method) from inside the callback.

solid answer

~40 s

afterCommit fires after the physical commit, and around that point Spring is unbinding the transactional resources (the EntityManager/connection) from the thread. So any persistence you attempt in afterCommit is not part of the just-committed transaction — depending on timing and the resource, it may silently do nothing, use an auto-commit connection, or throw because no transaction is active. The correct pattern is to not write directly in the callback but to delegate to a separate bean method annotated @Transactional(propagation = REQUIRES_NEW), which starts a brand-new transaction with its own connection. That new work commits independently of the original. This is exactly why @TransactionalEventListener(phase = AFTER_COMMIT) is often paired with a REQUIRES_NEW handler. Be aware the new transaction can itself fail after the first one already committed, so design for that inconsistency (idempotency, retries, outbox).

code

java · 25 lines
java
@Service
public class OrderService {
    private final AuditService auditService; // separate bean = real proxy

    @Transactional
    public void placeOrder(Order order) {
        orderRepo.save(order);
        Long id = order.getId();
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override public void afterCommit() {
                // original tx already committed & resources unbound ->
                // must run in a NEW transaction:
                auditService.recordAudit(id);
            }
        });
    }
}

@Service
class AuditService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void recordAudit(Long orderId) {
        auditRepo.save(new Audit(orderId)); // its own connection + commit
    }
}

go deeper

for a junior

Know afterCommit is after commit, so writes there aren't part of that transaction.

for a middle

Use a separate @Transactional(REQUIRES_NEW) bean method for post-commit writes.

for a senior

Explain resource unbinding, auto-commit/no-tx pitfalls, LazyInitializationException, and the proxy self-call trap.

for a principal

Address the dual-write consistency gap with outbox/idempotency and treat afterCommit persistence as an integration boundary.

## Why 'outside the transaction' A Spring transaction binds resources to the thread via `TransactionSynchronizationManager` — for JPA the `EntityManager` (through `EntityManagerHolder`), for plain JDBC the `Connection` (through `ConnectionHolder`). The commit sequence in `AbstractPlatformTransactionManager` is roughly: `triggerBeforeCommit` → `triggerBeforeCompletion` → `doCommit` → `triggerAfterCommit` → `triggerAfterCompletion` → **cleanup/unbind resources**. By the time `afterCommit` runs, the original transaction has *already committed*. The bound resource is on its way out; the logical unit of work is over. ## What actually happens if you write there Behavior varies by resource and exact timing, none of it good: - **JPA via a repository**: the original persistence context has been flushed and committed; there is no active transaction for a new write, so Spring Data / JPA may throw (no transaction) or, with an auto-commit fallback, do something you did not intend. Reads may work off a new connection; writes are unreliable. - **Plain JDBC through `DataSourceUtils.getConnection`**: you may get a fresh connection in **auto-commit** mode, so each statement commits by itself — not what a caller expecting a transaction assumes. - **Lazy-loading**: touching a lazy JPA association in afterCommit often throws `LazyInitializationException` because the persistence context is gone. The unifying point: **afterCommit is not covered by the original transaction**, so treat it as if you were outside `@Transactional` entirely. ## The safe pattern — start a new transaction Delegate the write to a *separate bean* method annotated `@Transactional(propagation = Propagation.REQUIRES_NEW)`. That opens a new physical transaction with its own connection and commits independently: ```java @Transactional(propagation = Propagation.REQUIRES_NEW) public void recordAudit(Long orderId) { auditRepo.save(new Audit(orderId)); } ``` Call it from the callback. It must be a call *through the proxy* (another bean, or self-injected proxy) — a plain `this.recordAudit(...)` self-call bypasses the proxy and gets no new transaction. ## Why REQUIRES_NEW and not REQUIRED At afterCommit time there is no active transaction to join, so `REQUIRED` would just start one anyway — but being explicit with `REQUIRES_NEW` documents intent and behaves correctly even in edge cases where synchronization state lingers. ## The inherent consistency gap The first transaction is already durable. If the new afterCommit transaction fails, you have a committed order but no audit row (or no published message). This is a genuine dual-write problem. Mitigations: - **Idempotency + retry** on the second operation. - **Transactional outbox**: within the original transaction, insert an outbox row; a separate poller publishes it — turning the second step into something recoverable. - Accept eventual consistency and monitor. ## Relationship to @TransactionalEventListener `@TransactionalEventListener(phase = AFTER_COMMIT)` handlers run at exactly this point, so the same rule applies: if the handler must persist, annotate it (or the method it calls) with `@Transactional(propagation = REQUIRES_NEW)`. There is even a documented nuance: a handler that itself opens a new transaction is fine; relying on the original context is not. ## When you do NOT need a new transaction If afterCommit only does non-DB work (send an email, publish to a broker, evict a cache, fire a metric), no transaction is needed at all — just handle failures out-of-band.

  • Why must the REQUIRES_NEW method live on a different bean (or self-injected proxy)?
    Spring's @Transactional works via a proxy. A plain this.method() self-invocation bypasses the proxy, so no new transaction is started; you'd again be running outside any transaction.
  • What consistency risk remains even after using REQUIRES_NEW, and how do you address it?
    The original transaction already committed, so if the new one fails you have a partial state (dual-write problem). Mitigate with idempotency + retries or a transactional outbox.

saying these in an interview costs you the question

  • Assuming afterCommit writes join the just-committed transaction
  • Doing repository.save(...) directly in afterCommit and expecting it to be transactional
  • Using a this.method() self-call for the REQUIRES_NEW work
  • Ignoring the dual-write inconsistency when the second transaction fails

context