Question 18 · Section 5

Why @Transactional Does Not Work with Self-Invocation?

4. AspectJ -> bytecode weaving (complex) 5. Avoid self-invocation 6. Check via TransactionSynchronizationManager

Language versions: English Russian Ukrainian

Junior Level

Self-invocation - when a method calls another method of the SAME class.

@Service
public class UserService {

    public void outer() {
        inner();  // @Transactional will NOT work!
    }

    @Transactional
    public void inner() { }
}

Why: Spring created a Proxy around the object, but the call to inner() goes directly via this, bypassing the Proxy!


Middle Level

How Proxy Works

[ Client ] -> [ Proxy ] -> [ TransactionInterceptor ] -> [ Target ]
                                                         |
                                                   [ outer() ]
                                                         |
                                                   [ inner() ] <- around proxy!

External call (works):

Client -> Proxy.inner() -> TransactionInterceptor -> Target.inner()

Self-invocation (does NOT work):

Target.outer() -> this.inner() -> Target.inner() <- Proxy skipped!

Why CGLIB Does Not Help

CGLIB creates a subclass, but:
  -> In CGLIB proxy - it is a subclass. When external code calls a method, it goes through MethodInterceptor. But when a method calls another method of the same class via `this`, the call goes directly to the original method code, bypassing the interceptor.

Which Other Annotations Do Not Work

@Async        -> not asynchronous
@Cacheable    -> not caching
@Validated    -> not validating
@Retryable    -> not retrying

-> All based on AOP Proxy!

Senior Level

Diagnostics

// Check for transaction:
TransactionSynchronizationManager.isActualTransactionActive();

// In debugger:
// Proxy: UserService$$EnhancerBySpringCGLIB$$abc123
// Target: UserService

Memory: JDK vs CGLIB Proxy

JDK Proxy:
  -> com.sun.proxy.$Proxy123
  -> Implements interfaces

CGLIB:
  -> UserService$$EnhancerBySpringCGLIB$$abc123
  -> Inherits class

Production Experience

Real scenario: frequent cause of bugs

Developers write:
  @Transactional method calls another @Transactional method

-> Inner @Transactional is ignored
-> Data saved without transaction
-> Bug discovered only in production

Self-invocation is one of the most common reasons why @Transactional "doesn't work" in real projects.

Solution: Code Review for self-invocation

Best Practices

  1. Split into different beans
  2. TransactionTemplate -> programmatic control
  3. Self-injection -> @Lazy + ObjectProvider
  4. AspectJ -> bytecode weaving (complex)
  5. Avoid self-invocation
  6. Check via TransactionSynchronizationManager

Summary for Senior

  • Proxy intercepts only EXTERNAL calls
  • this.method() -> directly, around Proxy
  • CGLIB does not help (this = Target)
  • Self-invocation - frequent reason @Transactional “doesn’t work”
  • TransactionSynchronizationManager -> for checking
  • Solution: different beans or TransactionTemplate

Interview Cheat Sheet

Must know:

  • Self-invocation = a method calls another method of the SAME class
  • Spring AOP works via proxy, but this.method() goes directly, bypassing proxy
  • This affects ALL AOP annotations: @Transactional, @Async, @Cacheable, @Validated, @Retryable
  • CGLIB also does not help - inside Target object, this refers to Target, not to the proxy subclass
  • Check for transaction: TransactionSynchronizationManager.isActualTransactionActive()
  • One of the most common reasons “@Transactional doesn’t work” in real projects
  • Diagnostics: in debugger proxy = $$EnhancerBySpringCGLIB$$, target = regular class

Common follow-up questions:

  • Why doesn’t CGLIB solve the problem? -> CGLIB intercepts calls from outside, but this.method() inside Target goes directly
  • Which other annotations do not work with self-invocation? -> All AOP-based: @Async, @Cacheable, @Validated, @Retryable
  • How to detect self-invocation in code? -> Code Review, calling a method of the same class inside a @Transactional method
  • When does the bug appear? -> In production, when data is saved without expected transaction

Red flags (DO NOT say):

  • “CGLIB solves self-invocation” - does not solve, this = Target
  • “@Transactional on private methods works” - Spring AOP only for public
  • “This is a rare problem” - one of the most common causes of bugs
  • “Just put @Transactional on the class” - self-invocation still does not work

Related topics:

  • [[13. What is a proxy in Spring]]
  • [[17. What does the Transactional annotation do]]
  • [[19. How to solve the self-invocation problem]]