skip to content

Walk through how a call to a @Transactional method flows through the TransactionInterceptor and advisor at runtime.

level: seniorimportance: should knowfreq 45%

answer

  1. advisor pointcut matches -> auto-proxy -> interceptor
  2. getTransaction / proceed / commit-or-rollback / cleanup
  3. PlatformTransactionManager + TransactionStatus
  4. TransactionSynchronizationManager binds resources to thread
  5. rollbackOn rule, participating -> rollback-only flag

basics

~20 s

The proxy hands the call to the TransactionInterceptor. It reads the method's transaction settings, asks the TransactionManager to start or join a transaction, runs the real method, then commits on success or rolls back on a matching exception, and cleans up.

solid answer

~40 s

At startup, the BeanFactoryTransactionAttributeSourceAdvisor's pointcut (backed by AnnotationTransactionAttributeSource) matches beans with @Transactional, so Spring's auto-proxy creator wraps them and installs the TransactionInterceptor as around advice. At call time: the proxy invokes the interceptor; it resolves the method's TransactionAttribute (propagation, isolation, readOnly, rollback rules), determines the TransactionManager, and calls createTransactionIfNecessary — which delegates to the PlatformTransactionManager's getTransaction to start or join a transaction and bind resources (e.g. the Hibernate Session / JDBC Connection) to the thread via TransactionSynchronizationManager. It then proceeds to the target method inside a try/catch. On success it calls commitTransactionAfterReturning; on a throwable it consults the rollback rules and either rolls back or commits; finally it restores the previous transaction info. This is standard AOP MethodInterceptor around-advice.

code

java · 21 lines
java
// Conceptual sketch of what TransactionInterceptor does around your method.
// (Real class: org.springframework.transaction.interceptor.TransactionInterceptor)
Object invokeWithinTransaction(Method method, Object target, MethodInvocation call) throws Throwable {
    TransactionAttribute attr = attributeSource.getTransactionAttribute(method, target.getClass());
    PlatformTransactionManager tm = determineTransactionManager(attr);

    TransactionStatus status = tm.getTransaction(attr);   // start or join, bind resources to thread
    Object result;
    try {
        result = call.proceed();                          // <-- your @Transactional method runs here
    } catch (Throwable ex) {
        if (attr.rollbackOn(ex)) {
            tm.rollback(status);                          // unchecked by default
        } else {
            tm.commit(status);                            // checked exception -> commit by default
        }
        throw ex;
    }
    tm.commit(status);                                    // normal return -> commit
    return result;
}

go deeper

for a junior

Know that a proxy/interceptor starts the transaction, runs your code, then commits or rolls back.

for a middle

Name the PlatformTransactionManager and the commit-vs-rollback branch on exceptions.

for a senior

Walk the full flow: attribute source, interceptor, getTransaction, TransactionSynchronizationManager, TransactionStatus, cleanup.

for a principal

Reason about advisor ordering with other aspects, rollback-only propagation hazards, and the blocking-vs-reactive transaction-manager split.

## Two phases: wiring, then invocation ### Phase 1 — startup wiring `@EnableTransactionManagement` (or Boot's auto-config) registers three collaborators: - **`AnnotationTransactionAttributeSource`** — knows how to read `@Transactional` off a method/class and turn it into a `TransactionAttribute` (a `RuleBasedTransactionAttribute` carrying propagation, isolation, timeout, readOnly, and rollback rules). - **`TransactionInterceptor`** — a `MethodInterceptor` (AOP Alliance around advice) that actually manages the transaction. - **`BeanFactoryTransactionAttributeSourceAdvisor`** — a `PointcutAdvisor` combining a pointcut ('does this method have a transaction attribute?') with the interceptor as its advice. Spring's **auto-proxy creator** (`InfrastructureAdvisorAutoProxyCreator`, installed by `AutoProxyRegistrar`) sees this advisor, tests each bean's methods against the pointcut, and where it matches, replaces the bean with a **proxy** (JDK or CGLIB) whose advice chain includes the interceptor. ### Phase 2 — per invocation When an external caller invokes an advised method: 1. The **proxy** receives the call and dispatches into the advice chain, reaching `TransactionInterceptor.invoke(MethodInvocation)`. 2. The interceptor calls `getTransactionAttribute` on the attribute source to obtain the method's `TransactionAttribute` (cached after first lookup). 3. It determines the **`TransactionManager`** — a `PlatformTransactionManager` (e.g. `JpaTransactionManager`, `DataSourceTransactionManager`) resolved by qualifier or the single primary one. 4. `createTransactionIfNecessary` calls `PlatformTransactionManager.getTransaction(definition)`. Depending on **propagation**, this starts a new physical transaction, joins the current one, suspends it, etc. It binds the transactional resource (JDBC `Connection`, JPA `EntityManager`) to the current thread through **`TransactionSynchronizationManager`**, and stores a `TransactionInfo` on a thread-local stack. 5. The interceptor calls `invocation.proceed()` — running **your actual method** (and any further advice like `@Cacheable`, subject to advisor order). 6. **On normal return:** `commitTransactionAfterReturning` → `PlatformTransactionManager.commit(status)`. For an outer/new transaction this flushes and commits; for a participating (joined) one it usually just marks completion. 7. **On a `Throwable`:** `completeTransactionAfterThrowing` consults the `TransactionAttribute.rollbackOn(ex)` rule. Default: roll back on `RuntimeException`/`Error`, commit on checked exceptions (unless `rollbackFor`). If rollback is chosen and the transaction is participating, it sets the shared transaction **rollback-only** flag (which is what produces `UnexpectedRollbackException` for an outer caller). 8. **Finally:** `cleanupTransactionInfo` restores the previously bound `TransactionInfo`, unbinding resources and resuming any suspended transaction. ## Key collaborators to name - `PlatformTransactionManager` — the abstraction; `getTransaction`, `commit`, `rollback`. - `TransactionDefinition` / `TransactionAttribute` — the settings. - `TransactionStatus` — the handle for the running transaction (isNewTransaction, setRollbackOnly). - `TransactionSynchronizationManager` — thread-bound resource + synchronization registry. ## Why this matters - **Thread-bound**: the whole model is thread-local, which is why blocking `@Transactional` doesn't compose with reactive code (there's a separate `ReactiveTransactionManager` / `TransactionalOperator`). - **Advisor order**: because it's just an advisor, its `order` relative to other aspects (security, caching, retry) determines nesting. E.g. `@Retryable` must sit *outside* `@Transactional` to retry a fresh transaction. - **rollback-only propagation**: an inner participating method that throws can doom the outer commit even if the outer catches the exception — a classic 'Transaction silently rolled back' surprise. ## When to reach past it For fine-grained control (partial commits, savepoints beyond NESTED, programmatic scope), use `TransactionTemplate` or the `PlatformTransactionManager` directly instead of the declarative interceptor.

  • Where are the transactional resources (Connection / EntityManager) stored during the call?
    In TransactionSynchronizationManager, which binds them to the current thread via thread-locals. That thread-bound design is also why the blocking model doesn't carry into reactive pipelines.
  • An inner participating method throws and rolls back, but the outer method catches it and returns normally. What does the outer caller see?
    An UnexpectedRollbackException at commit. The inner rollback set the shared transaction's rollback-only flag, so the outer commit cannot succeed even though the exception was swallowed.

saying these in an interview costs you the question

  • Thinking the interceptor opens a JDBC connection per method rather than delegating to a PlatformTransactionManager
  • Not knowing the model is thread-bound (and thus incompatible with reactive without a ReactiveTransactionManager)
  • Believing advisor order relative to @Retryable/@Cacheable doesn't matter

context