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?
answer
- afterCommit = resources already unbound
- direct write → no active tx / auto-commit / throws
- delegate to REQUIRES_NEW bean method
- self-call bypasses proxy = no new tx
- dual-write gap → outbox / idempotency
basics
~20 sBy 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 safterCommit 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@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
Know afterCommit is after commit, so writes there aren't part of that transaction.
Use a separate @Transactional(REQUIRES_NEW) bean method for post-commit writes.
Explain resource unbinding, auto-commit/no-tx pitfalls, LazyInitializationException, and the proxy self-call trap.
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