π‘ Lesson Learned — Not a Prod Bug, But a Real Pain
No, this wasn’t a production outage.
Nobody screamed at me.
But I sat for 3 hours wondering:
“Why the heck is my@Transactional
not rolling back!?” π΅π«
“Why is Redis cache not working?” π€―
Turned out, the issue was one silent villain:
π§± Self-invocation
π€· What Is @Transactional
?
If you're new:
@Transactional
= Tells Spring to start a DB transaction when a method is called.
It’ll commit if everything’s okay.
It’ll rollback if something fails.
π§ Think of it like wrapping your code in:
try {
beginTransaction();
// your logic
commit();
} catch(Exception e) {
rollback();
}
π΅️ Real-Life Analogy — The Gateway Community π️
Let me tell you about my society — it has a strict watchman at the gate. Here’s how it works:
- π Watchman = Spring Proxy
- π Your apartment = Your service class
- πͺ Your room = A method inside that class
π Scenario 1: Outsider Visits
Your friend from outside the society tries to visit you.
The watchman stops him at the gate.
Asks for ID, checks the visitor list, logs entry, and then lets him in.
π― This is like Spring calling your method from another bean:
someOtherService.callYourMethod(); // ✅ Proxy intercepts
- π Transaction starts
- π§ Cache is checked
- π₯ Circuit breakers are applied
π§♂️ Scenario 2: Your Neighbor Walks Into Your Room
But if your next-flat neighbor (same building) enters directly through the balcony, skipping the gate:
The watchman doesn't even know.
No checks. No logging. No nothing.
π± That’s what happens when a method in the same class calls another annotated method:
public void yourOwnMethod() {
this.annotatedMethod(); // ❌ No proxy = Spring does nothing
}
π§ So the rule is simple:
π¬ “Spring’s watchman (proxy) only watches external visitors.
If you're inviting yourself or your neighbor is hopping over from the balcony...
He doesn't care.” π
π« Annotations That Won’t Work With Self-Call
These annotations rely on Spring Proxies.
But if you call another method inside the same class, the proxy is bypassed = annotations don’t work.
⚠️ FULL LIST — Don’t Miss These!
π Annotation | ⚠️ Why It Breaks in Self-Call | π‘ What It Does |
---|---|---|
@Transactional | No transaction started | DB commit/rollback |
@Cacheable | Cache not checked | Caches method result |
@CachePut | Cache not updated | Updates cache |
@CacheEvict | Cache not cleared | Evicts cache |
@Async | Runs in same thread | Runs in separate thread |
@Scheduled | Won’t auto-trigger self | Runs on cron/interval |
@Retryable | Won’t retry method | Auto-retry failed methods |
@RateLimiter | No limit applied | Restricts method rate |
@CircuitBreaker | Circuit not opened/closed | Handles failure gracefully |
@TimeLimiter | No timeout applied | Cancels slow methods |
@Bulkhead | No isolation | Limits concurrent executions |
π₯ WHY Do These Fail in Self-Call?
Because Spring uses Proxy-Based AOP (Aspect-Oriented Programming).
In Simple Words:
Spring wraps your bean like this:
[ProxyBean (adds features like @Transactional)]
|
↓
[RealBean]
If someone else calls the method → It goes through proxy ✅
If you call your own method → You talk to yourself ❌
𧬠Diagram — What Happens Behind the Scenes
YOU (Controller/Another Class)
|
call bean.method()
↓
[ Spring Proxy Layer ]
(Starts transaction / checks cache / etc.)
↓
[ Your Method ]
❌ But if you do this.someMethod()
from inside the same class:
YOU (Same Class)
|
call this.method() → Direct call
↓
[ Your Method ] ← Skips Proxy = No annotation behavior
⚖️ Rules: When @Transactional
Won’t Work (Must-Know!)
π§ Scenario | ✅ Will It Work? | π§ Why |
---|---|---|
Method is private | ❌ | Proxy can’t access |
Method is final | ❌ | Proxy can't override |
Self-invocation (same class) | ❌ | Proxy not involved |
Checked exception w/o rollbackFor | ❌ | No rollback |
Non-Spring-managed class | ❌ | No proxy created |
Called from another bean | ✅ | Goes through proxy |
Public + throws RuntimeException | ✅ | All Spring conditions met |
π§ͺ Real-Time Example — Happened to Me π€¦♂️
@Service
public class UserService {
@Cacheable("userCache")
public User getUser(Long id) {
// fetch from DB
}
public User getUserWithLog(Long id) {
log.info("Fetching user...");
return getUser(id); // ❌ self-invocation = no cache
}
}
Result:
❌ No cache hit
❌ DB called every time
π’ I cried, again
π ️ Fix Options
1️⃣ Move method to another bean
@Service
public class CacheService {
@Cacheable("userCache")
public User getUser(Long id) { ... }
}
@Service
public class UserService {
@Autowired CacheService cacheService;
public User getUserWrapper(Long id) {
return cacheService.getUser(id); // ✅ proxy works
}
}
π€Ή Funny Analogy
You wear a parachute (@Transactional
) but jump from a trampoline inside your house.
The parachute doesn’t open. You didn’t go outside (i.e., through proxy). π
π Wrapping Up
✅ If your Spring annotation isn’t working — ask this:- ❓ Is the method being called from another bean?
- ❓ Is the method public?
- ❓ Is there any self-invocation?
- ❓ Is the annotation even meant to work in this flow?
π’ Stay Tuned for Tomorrow's Post
π Tomorrow we go deeper:
“Why@Transactional
only rolls back onRuntimeException
– And how to fix it”
π£ Spoiler: It’s not as simple as it looks!
Comments
Post a Comment