skip to content

Why does manager-driven transaction management avoid the proxy self-invocation problem, and what are the trade-offs versus @Transactional?

level: seniorimportance: should knowfreq 30%

answer

  1. @Transactional = proxy; boundary at proxy crossing
  2. self-invocation / private / final bypass the proxy
  3. manager: no proxy, boundary at getTransaction() call
  4. immune to self-invocation, works in private code
  5. cost: boilerplate + tangled, scattered concern

basics

~20 s

Because there is no AOP proxy involved — the transaction starts because your code calls getTransaction() directly, not because a proxy intercepted a call. So @Transactional's self-invocation gotcha (private/internal calls bypassing the proxy) simply doesn't exist. The cost is more boilerplate.

solid answer

~40 s

@Transactional relies on a Spring AOP proxy: the transaction only starts when a call crosses the proxy boundary. Internal self-invocation (a method calling another @Transactional method on 'this'), private/final methods, and non-Spring-managed instances all bypass the proxy, so the annotation silently does nothing. Manager-driven transactions have no proxy — you explicitly call txManager.getTransaction(...), so the boundary is wherever your code is, regardless of who called it or its visibility. That makes it robust for helper/private methods, framework code, and cases where proxying is unavailable. Trade-offs: you write the try/catch/commit/rollback boilerplate, lose declarative composition and rollback rules, and the boundary is scattered in code rather than declared. Most teams keep @Transactional as default and drop to the manager only for the awkward cases.

code

java · 24 lines
java
@Service
public class ReportService {

    private final PlatformTransactionManager txManager;

    public ReportService(PlatformTransactionManager txManager) { this.txManager = txManager; }

    public void run() {
        // Self-invocation of a private tx method: @Transactional on process() would be IGNORED
        // (no proxy crossing). The manager works here because there is no proxy at all.
        process();
    }

    private void process() {
        TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition());
        try {
            // ... work inside a real transaction, even though we're in a private method ...
            txManager.commit(status);
        } catch (RuntimeException ex) {
            txManager.rollback(status);
            throw ex;
        }
    }
}

go deeper

for a junior

Know that @Transactional uses a proxy and the manager doesn't.

for a middle

Explain the self-invocation gotcha and why the manager avoids it.

for a senior

Weigh trade-offs and recommend refactoring-into-a-bean before going programmatic.

for a principal

Discuss keeping cross-cutting concerns declarative and reserving the manager for infrastructure/fine-grained boundaries.

## How @Transactional's proxy works Spring implements `@Transactional` with a **proxy** around the bean (JDK dynamic proxy or CGLIB subclass). When an *external* caller invokes an annotated **public** method **through the proxy**, `TransactionInterceptor` opens a transaction, runs the target, then commits/rolls back. The transaction exists because the call *passed through the proxy*. ## The self-invocation / visibility gotchas Because it depends on crossing the proxy, `@Transactional` is silently skipped when: - **Self-invocation:** `this.otherTxMethod()` calls the real object directly, not the proxy — no transaction is started for `otherTxMethod`. - **Private / final / package-private methods:** the proxy can't intercept them (CGLIB can't override `final`; annotations on non-public methods are ignored by default). - **The bean isn't Spring-managed**, or you hold a raw (unproxied) reference. These produce the notorious 'my @Transactional does nothing' bug. ## Why the manager sidesteps all of it With the direct `PlatformTransactionManager`, **there is no proxy and no interception**. The transaction begins the instant *your code* runs `txManager.getTransaction(def)` — inside a private method, a static helper, a lambda, a call from `this`, anywhere. Visibility and call origin are irrelevant because nothing is being intercepted. So it is a reliable way to get a transaction exactly where you need one, immune to the self-invocation trap. `TransactionTemplate` shares this property (it's also programmatic). ## Trade-offs **In favor of the manager / programmatic:** - No self-invocation or visibility pitfalls. - Boundary is explicit and testable; you can start/commit at precise points. - Works in infrastructure code where AOP isn't set up. **Against:** - Boilerplate: definition + try/catch + commit + rollback in every place. - No declarative rollback rules or `rollbackFor` convenience. - Transaction concerns get **tangled into business code** (the very thing declarative style removes) and are scattered rather than centralized. - Harder to compose/nest cleanly; propagation you get 'for free' with annotations must be reasoned about manually. ## Practical guidance Default to `@Transactional` for service methods (clean, declarative). If you hit self-invocation, first consider refactoring the inner method into a **separate bean** so the call crosses a proxy — that's usually cleaner than going programmatic. Reach for the direct manager (or `TransactionTemplate`) when: you need a transaction inside a genuinely private/helper path, you need fine-grained per-iteration boundaries, or you're writing framework/utility code with no proxy available. The manager is the escape hatch, not the default.

  • A colleague put @Transactional on a private method and it does nothing. Should they switch to the manager?
    Not necessarily. The idiomatic fix is to move that method into a separate Spring bean so the call crosses the proxy, keeping declarative style. Drop to the manager/TransactionTemplate only when refactoring isn't practical or you need finer control.

saying these in an interview costs you the question

  • Saying @Transactional works on self-invoked or private methods
  • Claiming the manager also uses a proxy
  • Recommending programmatic transactions as the default instead of the escape hatch

context