Proxies & Common Pitfalls

Overview

Spring’s most famous “wait, why isn’t this working?” bugs almost all trace back to proxies. Features like @Transactional, @Async, @Cacheable, and @Secured are implemented by wrapping your bean in a proxy — and a proxy can only intercept calls that go through it.

Official: AOP Proxies.


Table of contents

  1. Why Spring uses proxies
  2. JDK proxy vs CGLIB
  3. Self-invocation — the #1 bug
  4. @Transactional on private methods
  5. @Configuration proxy modes
  6. @Cacheable + @Transactional order
  7. final classes and methods
  8. Interview questions
  9. Summary

1. Why Spring uses proxies

When you put @Transactional on a method, Spring creates a proxy of your bean. The proxy:

  1. Receives the call.
  2. Opens a transaction.
  3. Calls your real method.
  4. Commits or rolls back.

If the call doesn’t go through the proxy, none of that happens.


2. JDK proxy vs CGLIB

JDK dynamic proxy CGLIB
Works by Implementing your interfaces Subclassing your concrete class
Requires Interface None (but class can’t be final)
Can advise Interface methods only All non-final methods

Modern Spring Boot defaults to CGLIB for class-based beans. The takeaway: don’t mark beans final and don’t mark advised methods final.


3. Self-invocation — the #1 bug

@Service
public class OrderService {

    public void place(Order o) {
        save(o);              // calls THIS.save() — no proxy
    }

    @Transactional
    public void save(Order o) { ... }   // NOT transactional when called from place()
}

this.save(...) is a direct method call. The proxy never sees it, so @Transactional is ignored.

Fixes:

Fix When
Move save to a different bean Best — proper separation
Inject OrderService into itself (self) Last resort
Apply transactionality on the public entry method (place) Often the simplest fix

The same trap applies to @Async, @Cacheable, @Validated, @Secured.


4. @Transactional on private methods

A private method is invisible to the proxy. Spring does not intercept it. Make the method public and call it through the proxy, or move it to another bean.


5. @Configuration proxy modes

@Configuration(proxyBeanMethods = true) (the default) makes Spring proxy the @Configuration class so that calling one @Bean method from another returns the container’s singleton, not a fresh instance.

@Configuration
public class AppConfig {
    @Bean Foo foo() { return new Foo(); }
    @Bean Bar bar() { return new Bar(foo()); }   // returns the singleton foo
}

Set proxyBeanMethods = false (lite mode) for slightly faster startup — but then calls between @Bean methods create new instances. Use lite only when bean methods don’t call each other.


6. @Cacheable + @Transactional order

If you put both on the same method, the order of aspects matters. By default Spring opens the transaction before checking the cache, which is usually what you want — but it’s worth verifying for your version when mixing aspects, since aspect order can be overridden.


7. final classes and methods

CGLIB cannot subclass a final class or override a final method. Result: the proxy fails or silently doesn’t apply. Avoid final on beans and on advised methods.


8. Interview questions

Q: Why doesn’t @Transactional work when I call the method from the same class? A: Self-invocation goes via this, which bypasses the Spring proxy. Move the method to another bean or call through an injected reference to self.

Q: Can I put @Transactional on a private method? A: Not reliably. The proxy can’t intercept it. Make it public.

Q: What’s the trade-off of @Configuration(proxyBeanMethods = false)? A: Faster startup, but @Bean methods calling each other within the class don’t return singletons — they create new instances.

Q: CGLIB and final classes? A: CGLIB can’t subclass final classes, so proxying fails. Don’t mark beans or advised methods final.

Q: What’s the difference between JDK and CGLIB proxies? A: JDK proxies implement interfaces; CGLIB proxies subclass the concrete class. CGLIB is the default for class-based beans.


9. Summary

Smell Likely cause
@Transactional ignored Self-invocation or private method
@Async runs on caller thread Self-invocation or missing @EnableAsync
@Cacheable not caching Self-invocation
Random proxy failure final class/method
@Bean returning fresh instances proxyBeanMethods = false

Next steps


Happy Learning

Next: Master Interview Q&A