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
- Why Spring uses proxies
- JDK proxy vs CGLIB
- Self-invocation — the #1 bug
@Transactionalon private methods@Configurationproxy modes@Cacheable+@Transactionalorderfinalclasses and methods- Interview questions
- Summary
1. Why Spring uses proxies
When you put @Transactional on a method, Spring creates a proxy of your bean. The proxy:
- Receives the call.
- Opens a transaction.
- Calls your real method.
- 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