skip to content

@Transactional / @Cacheable / @Async on Proxies

@Transactional, @Cacheable, @Async, @Validated, @Retryable and method security are all advisors on the same proxy, so they share the same self-invocation and visibility caveats. Recognizing the shared mechanism turns several separate gotchas into one rule.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

Why does calling one @Transactional method from another method in the same class sometimes leave the transaction annotation ignored?

level: juniorimportance: must knowfreq 82%

answer

  1. proxy wraps bean, this.method() skips it
  2. internal call = raw target, no advice
  3. self-inject / separate bean / AopContext
  4. AspectJ weaving is the only true fix
  5. no error, silently ignored

basics

~10 s

Spring's @Transactional works through a proxy that wraps the bean. An internal call (this.method()) bypasses the proxy, so the transaction advice never runs. Only calls coming through the injected bean reference are intercepted.

solid answer

~40 s

Declarative features like @Transactional are applied by a proxy Spring puts around your bean. When another bean calls the method, the call goes through that proxy, which starts the transaction before delegating to your real object. But when a method inside the same class calls a sibling method with plain this.method(), the call targets the raw object directly, so the proxy — and its transaction advice — is skipped entirely. This is the self-invocation problem. It applies to all proxy-based declarative annotations: @Transactional, @Cacheable, @Async, @Retryable, @Validated. Fixes: move the annotated method into a separate bean, inject the bean into itself and call through the injected reference, or use AopContext.currentProxy(). The cleanest fix is usually redesign so the entry point is called externally.

code

java · 28 lines
java
@Service
public class OrderService {

    // Called externally -> goes through proxy -> transaction ignored below anyway
    public void placeOrder(Order o) {
        // this.save(o) is a raw call on the target object:
        // the @Transactional proxy is BYPASSED, no transaction is started.
        save(o);
    }

    @Transactional
    public void save(Order o) {
        repository.insert(o); // runs WITHOUT a transaction when reached via placeOrder()
    }
}

// Fix via self-injection:
@Service
public class OrderService2 {
    @Autowired private OrderService2 self; // injected reference IS the proxy

    public void placeOrder(Order o) {
        self.save(o); // now goes through the proxy -> @Transactional applies
    }

    @Transactional
    public void save(Order o) { repository.insert(o); }
}

go deeper

for a junior

Should know the proxy exists and that internal calls can skip @Transactional.

for a middle

Should articulate this.method() targets the raw object and list the standard fixes.

for a senior

Should connect it to all proxy-based annotations and reason about which fix fits which design.

for a principal

Should weigh AspectJ weaving vs restructuring, and understand the maintenance cost of self-injection/AopContext hacks.

## The mechanism Spring implements declarative annotations such as `@Transactional`, `@Cacheable`, `@Async`, `@Retryable`, `@Validated`, and method security using **AOP proxies**. When Spring detects one of these annotations on a bean, it does not put the logic inside your class — instead it wraps your bean in a **proxy object** that shares the same interface/type. The proxy holds a reference to your real object (the *target*). Every method call that arrives at the proxy first runs the **advice** (e.g. `TransactionInterceptor` opens a transaction), then delegates to the target, then runs the after-part (commit/rollback). Crucially, **only the proxy runs the advice.** Your real object knows nothing about transactions. ## Why self-invocation breaks it When another bean has your bean injected, it holds a reference to the **proxy**. Calling `service.doWork()` goes proxy → advice → target. Good. But inside your class, when `methodA()` calls `methodB()`, the compiler emits `this.methodB()`. `this` is the **raw target object**, not the proxy. So the call goes straight to the target, skipping the proxy and all advice. `@Transactional`/`@Cacheable`/`@Async` on `methodB` are silently ignored. No error, no warning — just no transaction, no caching, no async execution. ## Fixes 1. **Restructure (preferred):** put the annotated method in a *different* bean, so the call crosses a bean boundary and goes through that bean's proxy. 2. **Self-injection:** inject the bean into itself and call `self.methodB()`. The injected reference is the proxy. ```java @Autowired private MyService self; ``` 3. **AopContext.currentProxy():** requires `@EnableAspectJAutoProxy(exposeProxy = true)` (or `exposeProxy=true` on `@EnableTransactionManagement` is not a thing — use the proxy-exposure flag). Then `((MyService) AopContext.currentProxy()).methodB()`. 4. **Switch to AspectJ load-time/compile-time weaving** (`mode = AdviceMode.ASPECTJ`): the advice is woven into the bytecode of the class itself, so even `this.methodB()` is intercepted. This is the only fix that makes self-invocation *just work*, but it needs a weaving agent. ## Related gotchas - The proxy only wraps **Spring-managed beans**. A `new MyService()` is never advised. - Proxy-based advice cannot see **private** or (for CGLIB) **final** methods. - Public constructor logic runs before advice too. ## When to care Any time a `@Transactional`/`@Cacheable`/`@Async` method mysteriously doesn't take effect, self-invocation is the first suspect.

  • Name three ways to make an internal call actually honor @Transactional.
    Move the method to a separate bean so the call crosses a proxy boundary; self-inject the bean and call through the injected (proxy) reference; use AopContext.currentProxy() with exposeProxy=true; or switch to AspectJ weaving (mode=ASPECTJ) which is the only option that intercepts this.method() directly.
  • Does self-invocation only affect @Transactional?
    No. It affects every proxy-based declarative annotation: @Cacheable/@CacheEvict, @Async, @Retryable, @Validated, and method-level @PreAuthorize/@Secured. They all rely on the proxy, so all are bypassed on internal this.method() calls.

saying these in an interview costs you the question

  • Claiming @Transactional works regardless of how the method is called
  • Saying the fix is to make the method static or private (that makes it worse)
  • Believing Spring logs a warning when self-invocation skips advice

context

open as a page

What visibility and modifier constraints must a method satisfy for proxy-based Spring annotations to advise it, and why?

level: middleimportance: should knowfreq 58%

basics

~20 s

Proxy-based advice only applies to public methods (and by default the target must be a Spring bean). With CGLIB proxies the class and method can't be final, because CGLIB subclasses the target to override methods. Private, static, and final methods aren't advised.

open as a page

What are the practical caveats of @Async — how does it execute, and what makes it silently not run asynchronously or lose exceptions?

level: seniorimportance: should knowfreq 55%

basics

~20 s

@Async makes a method run on a separate thread via a proxy that submits it to a TaskExecutor. It needs @EnableAsync, a public method, and an external call (self-invocation runs it synchronously). Return void or CompletableFuture/Future; exceptions from void methods are lost unless you set an AsyncUncaughtExceptionHandler.

open as a page

A @Cacheable method's cache is never hit when called from within the same service. Diagnose and give the fix you'd ship.

level: seniorimportance: should knowfreq 48%

basics

~20 s

The internal call bypasses the caching proxy, so the CacheInterceptor never runs — every call recomputes. Fix by calling the @Cacheable method through the proxy: put it in a separate bean, self-inject the bean, or use AspectJ weaving. Restructuring into a separate bean is the cleanest.

open as a page

Contrast proxy-based weaving with AspectJ weaving for Spring's declarative annotations, and explain how you'd control ordering when several (e.g. @Transactional, @Cacheable, method security, @Retryable) apply to one method.

level: principalimportance: should knowfreq 33%

basics

~20 s

Proxy-based weaving wraps beans at runtime (JDK/CGLIB) and only intercepts external calls to public methods. AspectJ weaving edits bytecode at compile/load time, so it also advises private/final methods and self-invocation. When several advisors hit one method they run as nested layers; control the nesting with each advisor's order (e.g. @EnableTransactionManagement(order=...), Ordered/@Order), lowest order = outermost.

open as a page