Part 3 — The Self Invocation Problem in Spring Transactions (Why @Transactional Sometimes Doesn’t…
1. Introduction
Part 3 — The Self Invocation Problem in Spring Transactions (Why @Transactional Sometimes Doesn’t Work)
1. Introduction
Have you ever added @Transactional to a method, tested your code, and watched in confusion as the transaction silently did not work? The data was saved anyway (or worse, partially saved), and you spent hours debugging, only to find that the transaction annotation was simply… ignored?
You’re not alone. This is one of the most common and frustrating issues Spring developers encounter.
In the previous article, we learned how Spring implements transactions internally using AOP and proxy objects. Spring creates a proxy around beans annotated with @Transactional, and this proxy intercepts method calls to manage transaction behavior.
However, this proxy‑based approach introduces an important limitation known as the Self Invocation Problem.

In this article, we will understand:
- What self invocation means
- Why
@Transactionalsometimes does not work - How Spring proxies cause this issue
- Multiple ways to fix it in real applications
- Best practices to avoid this pitfall
This is a very common interview question and a real‑world bug that can corrupt data if not understood properly.
2. What is Self Invocation?
Self invocation happens when a method inside a class calls another method of the same class.
Example
@Service
public class EmployeeService {
public void processEmployee() {
updateEmployee(); // internal call (self invocation)
}
@Transactional
public void updateEmployee() {
// database update logic
employeeRepository.save(new Employee());
}
}
Here:
processEmployee()callsupdateEmployee()- Both methods belong to the same class (
EmployeeService)
3. The Expected Behavior
A developer writing this code might expect the following flow:
processEmployee() called
↓
updateEmployee() called
↓
@Transactional starts a transaction
↓
Database update occurs inside a transaction
↓
Transaction commits (or rolls back on error)
But this is NOT what happens.
4. What Actually Happens
Because the call happens inside the same class, the Spring proxy is bypassed entirely.
Actual Flow
Controller
↓
EmployeeService.processEmployee() ← called on the proxy
↓
Direct call to updateEmployee() ← uses 'this', NOT the proxy
↓
Proxy NOT involved
↓
@Transactional ignored
↓
Database update runs WITHOUT a transaction
Visual Diagram

The red path shows the problem: updateEmployee() is called directly on the original object (this), so the proxy never gets a chance to start a transaction.
5. Why This Happens (The Technical Reason)
As explained in Part 2, Spring transactions work using proxy objects. The architecture looks like this:
Normal (Working) Transaction Flow

Transactions are applied only when the method call goes through the proxy.
The Problem: Internal Calls Use this
When processEmployee() calls updateEmployee() internally, Java uses the current object reference (this). In a proxied Spring bean, this refers to the original object, not the proxy.
EmployeeService Proxy (managed by Spring)
↑
| (outside call goes through proxy)
|
Original EmployeeService object (contains 'this')
|
| (internal call uses 'this' → bypasses proxy)
↓
updateEmployee() called directly
Since the proxy is skipped, Spring never intercepts the call, and the @Transactional annotation is ignored.
6. Complete Example Scenario
Let’s see this in a complete, runnable example:
@Service
public class EmployeeService {
private final EmployeeRepository employeeRepository;
public EmployeeService(EmployeeRepository employeeRepository) {
this.employeeRepository = employeeRepository;
}
public void processEmployee() {
System.out.println("Inside processEmployee - about to call updateEmployee");
updateEmployee(); // self invocation!
}
@Transactional
public void updateEmployee() {
System.out.println("Inside updateEmployee - saving employee");
employeeRepository.save(new Employee("John"));
// If an exception occurs here, it should roll back
}
}
Test it:
@RestController
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@GetMapping("/test")
public String test() {
employeeService.processEmployee(); // ← calls the proxy
return "Check logs and database";
}
}
What you’ll see in the logs:
- Inside processEmployee — about to call updateEmployee
- Inside updateEmployee — saving employee
- Employee saved to database
What you WON’T see:
- No transaction start log
- No transaction commit log
- If an exception occurs in
updateEmployee(), the employee is still saved because there's no transaction to roll back
7. When Transactions Work Correctly
Transactions work when the call comes from another bean, because the proxy is involved.
Correct Flow
@RestController
public class EmployeeController {
@Autowired
private EmployeeService employeeService; // ← this is the proxy!
public void createEmployee() {
employeeService.updateEmployee(); // ← call goes through proxy
}
}

Here the proxy intercepts the call, so the transaction works perfectly.
8. How to Fix the Self Invocation Problem
There are several ways to solve this issue. Let’s explore them from most recommended to least recommended.
Solution 1: Move the Method to Another Service (Recommended ✅)
The cleanest solution is to split the transactional logic into a separate service.
@Service
public class EmployeeService {
private final EmployeeTransactionService transactionService;
public EmployeeService(EmployeeTransactionService transactionService) {
this.transactionService = transactionService;
}
public void processEmployee() {
transactionService.updateEmployee(); // ← calls through proxy
}
}
@Service
public class EmployeeTransactionService {
@Transactional
public void updateEmployee() {
// database update logic
employeeRepository.save(new Employee());
}
}
Why this works: The call goes from EmployeeService (which may or may not be proxied) to EmployeeTransactionService (which is a separate bean). Spring injects the proxy of EmployeeTransactionService, so @Transactional is applied.
Pros:
- Clean separation of concerns
- Follows Single Responsibility Principle
- Easy to test
- No circular dependencies
Cons:
- Slightly more classes
- May feel like overkill for simple cases
Solution 2: Self-Injection (Use with Caution ⚠️)
You can inject the service into itself, then call the transactional method on the injected reference.
@Service
public class EmployeeService {
@Autowired
private EmployeeService self; // self-reference (proxy)
public void processEmployee() {
self.updateEmployee(); // ← calls through proxy
}
@Transactional
public void updateEmployee() {
employeeRepository.save(new Employee());
}
}
Why this works: self is the Spring‑managed proxy, not the original object. Calling self.updateEmployee() goes through the proxy.
Pros:
- Minimal code change
- Keeps all logic in one class
Cons:
- Creates a circular reference (Spring handles it, but it’s conceptually messy)
- Can confuse developers and static analysis tools
- May cause issues with some AOP configurations
- Harder to unit test (mocking
selfis awkward)
When to use: Only as a temporary fix or in very simple cases where creating a new service feels overkill. Not recommended for production codebases.
Solution 3: Use Programmatic Transaction Management
Instead of relying on @Transactional, you can manually control transactions using TransactionTemplate.
@Service
public class EmployeeService {
private final TransactionTemplate transactionTemplate;
private final EmployeeRepository employeeRepository;
public EmployeeService(TransactionTemplate transactionTemplate,
EmployeeRepository employeeRepository) {
this.transactionTemplate = transactionTemplate;
this.employeeRepository = employeeRepository;
}
public void processEmployee() {
updateEmployee(); // internal call is fine now
}
public void updateEmployee() {
transactionTemplate.executeWithoutResult(status -> {
employeeRepository.save(new Employee());
});
}
}
Why this works: TransactionTemplate directly uses the transaction manager, bypassing the need for proxies.
Pros:
- Works regardless of how the method is called
- Fine‑grained control over transaction boundaries
Cons:
- More verbose
- Mixes transaction logic with business logic
- Loses declarative style
When to use: When you need complex transaction logic (like multiple savepoints) or when you’re already using programmatic transactions elsewhere.
Solution 4: Use AspectJ Compile-Time Weaving (Advanced)
Spring can also use AspectJ to weave transaction logic directly into the bytecode, eliminating the need for proxies entirely.
@Configuration
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
public class TransactionConfig {
// configuration
}
Pros:
- Solves self invocation completely
- Works with internal calls
Cons:
- Requires compile‑time weaving or load‑time weaving setup
- More complex build process
- Not as commonly used in typical Spring Boot applications
When to use: Large enterprise applications where proxy limitations become a major issue and you can manage the build complexity.
9. Comparison of Solutions

10. Why This Knowledge is Important
Understanding the self invocation problem helps developers avoid common issues:
@Transactionalsilently not working — Data gets saved when it shouldn't, or not saved when it should- Unexpected database commits — Partial updates when exceptions occur
- Difficult debugging scenarios — Hours wasted wondering why transactions are ignored
- Data inconsistency — In production, this can corrupt business data
This concept is also frequently discussed in backend interviews. Knowing the why and how to fix it separates experienced Spring developers from beginners.
11. Common Interview Questions
Q1: What is the self invocation problem?
A: It occurs when a method inside a Spring bean calls another method of the same bean that is annotated with @Transactional. Because the call bypasses the Spring proxy, the transaction is not applied.
Q2: Why does self invocation bypass the proxy?
A: The proxy is only used for external calls. Internal calls use the this reference, which points to the original object, not the proxy.
Q3: How can you fix the self invocation problem? A: Move the transactional method to a separate service, use self‑injection, use programmatic transactions, or switch to AspectJ weaving.
Q4: Does @Transactional work on private methods?
A: No, because the proxy cannot access private methods. The same proxy limitation applies.
Key Takeaways
- Spring transactions work using AOP proxies
- The proxy intercepts method calls to apply transaction logic
- Self invocation (internal method calls) bypasses the proxy
- When the proxy is bypassed,
@Transactionalis ignored - Transactions work only when methods are called through the proxy
- Moving transactional logic to a separate service is the cleanest solution
- Self‑injection can work but is not recommended for production
- Programmatic transactions and AspectJ are alternative approaches
🧠 Quick Challenge for You!
Consider the following code:
@Service
public class UserService {
@Autowired
private UserService self;
public void process() {
update();
}
@Transactional
public void update() {
// update database
}
}
Questions:
- Will
update()run inside a transaction whenprocess()is called from a controller? - What change would make it transactional?
- Why is self‑injection considered less clean than a separate service?
👇 Drop your answers in the comments below! This is a common interview question.
💬 Let’s Discuss!
- Have you ever debugged a transaction that wasn’t rolling back due to self invocation?
- Which solution do you prefer and why?
- Any other proxy‑related pitfalls you’ve encountered?
Leave a comment below — let’s learn together! 👇
🔜 What’s Next
In the next article, we’ll dive into Hibernate Persistence Context.
👉 **Part 4 — Understanding Hibernate Persistence Context**
We’ll explore:
- What is a persistence context?
- How it relates to JPA and Hibernate
- The lifecycle of managed, detached, and transient entities
- How the persistence context enables features like dirty checking and lazy loading
- First-level cache and its role in performance
Understanding the persistence context is crucial before we tackle dirty checking and lazy loading in later articles.
📚 Spring Transactions & Hibernate Internals Series
- Part 1 — Transaction Management in Spring Boot
- Part 2 — How @Transactional Works Internally in Spring Boot
- Part 3 — The Self Invocation Problem in Spring Transactions 👈you are here
- Part 4 — Understanding Hibernate Persistence Context
- Part 5 — Dirty Checking in Hibernate
- Part 6 — Flush vs Commit in Hibernate
- Part 7 — Lazy Loading vs Eager Loading in Hibernate
- Part 8 — The N+1 Query Problem in Hibernate
- Part 9 — Interview Q&A
메타데이터
- post_id
- d2f9e1641b51
- slug
- the-self-invocation-problem-in-spring-transactions-why-transactional-sometimes-doesnt-work-d2f9e1641b51
- url
- https://medium.com/@varuntewani01/the-self-invocation-problem-in-spring-transactions-why-transactional-sometimes-doesnt-work-d2f9e1641b51
- canonical_url
- https://medium.com/@varuntewani01/the-self-invocation-problem-in-spring-transactions-why-transactional-sometimes-doesnt-work-d2f9e1641b51
- author_url
- https://medium.com/@varuntewani01
- status
- ok
- fetched_at
- 2026-07-13 06:23:13