Yes 😄. Let’s make @Transactional so simple that even a dog could understand it.
Think of a transaction like a dog carrying two biscuits. 🐕🍪🍪
🐕 First understand: What is a Transaction?
Imagine your dog has two jobs:
- Give one biscuit to Tom.
- Give another biscuit to Jerry.
Both should happen together.
🐕 Dog
│
├── 🍪 Give biscuit to Tom
│
└── 🍪 Give biscuit to Jerry
If both succeed:
✅ Tom gets biscuit
✅ Jerry gets biscuit
✅ DONE
But what if:
🍪 Tom gets biscuit
❌ Jerry cannot receive biscuit
We don’t want:
Tom → 🍪
Jerry → ❌
We want:
Tom → ❌
Jerry → ❌
ROLLBACK EVERYTHING
That’s basically what a database transaction does.
🐕 What does @Transactional mean?
When you write:
@Transactional
public void placeOrder() {
saveOrder();
savePayment();
}
Think of it as telling Spring:
🐕 “Start watching this entire operation. If everything goes well, keep the changes. If something goes wrong, undo the changes.”
So:
🐕 Spring
│
START TRANSACTION
│
▼
saveOrder()
│
▼
savePayment()
│
┌─────┴─────┐
│ │
SUCCESS ERROR
│ │
▼ ▼
COMMIT ROLLBACK
🐕 Reason #1 — Self Invocation
This is the #1 thing you need to understand.
Suppose:
@Service
public class OrderService {
public void createOrder() {
saveOrder();
}
@Transactional
public void saveOrder() {
// database operation
}
}
You may think:
createOrder()
↓
saveOrder()
↓
🐕 Transaction starts
But Spring says:
“Wait! Who called
saveOrder()?”
The answer is:
this.saveOrder();
The object called itself.
So Spring’s transaction proxy was never involved.
🐕 Imagine a security gate
Think of Spring’s proxy as a security gate.
🚪 Spring Proxy
│
▼
🏠 OrderService
When someone enters through the gate:
🚶
↓
🚪
↓
🏠
Spring can say:
“Start transaction!”
But self-invocation is:
🏠 OrderService
│
└──────► saveOrder()
The dog is already inside the house.
It never passed through the gate.
Therefore:
❌ Transaction interceptor not triggered
Solution
Put the transactional method in another service:
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void createOrder() {
paymentService.savePayment();
}
}
@Service
public class PaymentService {
@Transactional
public void savePayment() {
// database operation
}
}
Now:
OrderService
↓
🚪 Spring Proxy
↓
PaymentService
↓
@Transactional
✅ Transaction works.
🐕 Reason #2 — You used new
Look at this:
UserService service = new UserService();
service.saveUser();
The dog says:
“Is Spring managing this object?”
No.
You created it yourself.
new UserService()
↓
🐕 Normal Java object
↓
❌ Spring doesn't control it
↓
❌ No transaction proxy
Spring wants:
@Autowired
private UserService userService;
or preferably constructor injection:
private final UserService userService;
public OrderService(UserService userService) {
this.userService = userService;
}
Now:
Spring
↓
🚪 Proxy
↓
UserService
✅ Good.
🐕 Reason #3 — Method is private
You write:
@Transactional
private void saveUser() {
}
Dog says:
“Spring’s proxy can’t properly intercept this private method.”
Use:
@Transactional
public void saveUser() {
}
Think:
public method
↓
🚪 Proxy can intercept
↓
🐕 Transaction
But:
private method
↓
🚫 Proxy can't intercept normally
🐕 Reason #4 — You caught the exception
This one is VERY important.
Suppose:
@Transactional
public void order() {
saveOrder();
try {
savePayment();
throw new RuntimeException("Payment failed");
} catch (Exception e) {
System.out.println("Error");
}
}
What does Spring see?
saveOrder()
↓
savePayment()
↓
💥 Exception
↓
😎 catch()
↓
Method finishes normally
Spring sees:
"Everything looks fine!"
So:
COMMIT ❌
You swallowed the exception.
Better
@Transactional
public void order() {
saveOrder();
savePayment();
throw new RuntimeException("Payment failed");
}
Now:
💥 Exception
↓
Spring sees exception
↓
ROLLBACK
Or log and rethrow:
catch (Exception e) {
log.error("Payment failed", e);
throw e;
}
🐕 Reason #5 — Checked Exception
Consider:
@Transactional
public void order() throws Exception {
saveOrder();
throw new Exception("Failed");
}
Dog asks:
“Is this a RuntimeException?”
No.
It’s a checked exception.
By default, Spring doesn’t roll back for every checked exception.
If you want rollback:
@Transactional(rollbackFor = Exception.class)
public void order() throws Exception {
saveOrder();
throw new Exception("Failed");
}
Now:
Exception
↓
rollbackFor
↓
🐕 Spring
↓
ROLLBACK
🐕 Reason #6 — Database doesn’t support transactions
Imagine Spring says:
🐕 "ROLLBACK!"
But database says:
😐 "I don't support that."
Spring cannot perform a magical rollback if the underlying database/table/engine doesn’t support transactions properly.
For MySQL, normally you want:
InnoDB
Check:
SHOW CREATE TABLE users;
You want something like:
ENGINE=InnoDB
🐕 Reason #7 — Wrong Transaction Manager
Imagine you have two dogs:
🐕 Dog A → MySQL
🐕 Dog B → Oracle
And you accidentally tell Spring:
“Use Dog B.”
But your operation is actually happening on MySQL.
You can get confusing transaction behavior.
With multiple databases, you may need to explicitly specify:
@Transactional(transactionManager = "mysqlTransactionManager")
The exact configuration depends on your application.
🐕 Reason #8 — Wrong @Transactional import
There are different libraries that have annotations with the same name.
For normal Spring Boot transaction management, you generally want:
import org.springframework.transaction.annotation.Transactional;
If you accidentally import a different annotation, Spring may not apply the transaction behavior you’re expecting.
🐕 Reason #9 — Propagation misunderstanding
This one sounds scary but is actually easy.
Suppose:
@Transactional
public void order() {
paymentService.pay();
}
And:
@Transactional
public void pay() {
}
By default:
Propagation.REQUIRED
means:
“If a transaction already exists, join it.”
So:
order()
│
│ Transaction T1
▼
pay()
│
└── joins T1
There is normally one transaction.
But:
@Transactional(propagation = Propagation.REQUIRES_NEW)
means:
“Stop the current transaction and start a completely new one.”
So:
order()
│
▼
Transaction T1
│
▼
pay()
│
▼
Pause T1
│
▼
Start T2
│
▼
pay()
│
▼
Commit T2
│
▼
Resume T1
This is very important in real applications.
🐕 Reason #10 — Transaction is at the wrong place
Don’t think:
Controller
↓
@Transactional
↓
Repository
A better design is usually:
Controller
↓
Service
↓
@Transactional
↓
Repository
↓
Database
For example:
@RestController
public class OrderController {
@PostMapping("/order")
public void createOrder() {
orderService.createOrder();
}
}
Service:
@Service
public class OrderService {
@Transactional
public void createOrder() {
orderRepository.save(order);
paymentRepository.save(payment);
}
}
The service layer is usually the natural transaction boundary because it represents a business operation.
🐕 Now let’s see the COMPLETE picture
Imagine you’re building a banking application.
Customer transfers:
₹10,000
From:
Account A
To:
Account B
You have two operations:
@Transactional
public void transferMoney() {
debitAccountA();
creditAccountB();
}
Without transaction:
Account A
₹50,000
↓
Debit ₹10,000
↓
₹40,000
Account B
₹20,000
↓
❌ Credit failed
↓
₹20,000
💀 Money disappeared logically.
With transaction:
START TRANSACTION
↓
Debit A
↓
Credit B
↓
Everything successful?
↓
YES
↓
COMMIT
But if credit fails:
START TRANSACTION
↓
Debit A
↓
Credit B
↓
💥 ERROR
↓
ROLLBACK
↓
Debit A is also undone
Final:
Account A → ₹50,000
Account B → ₹20,000
🐕 Dog happy. Money safe.
🧠 The most important mental model
Don’t memorize 10 reasons separately.
Remember this:
@Transactional
│
▼
"Spring Proxy"
│
┌───────────┴───────────┐
│ │
Enters proxy Bypasses proxy
│ │
▼ ▼
Transaction No transaction
So whenever @Transactional isn’t working, ask:
Question 1
Did the call go through a Spring proxy?
Check:
❌ self-invocation
❌ new Service()
❌ object not managed by Spring
❌ problematic method visibility
Question 2
Did an exception reach Spring?
Check:
❌ caught and swallowed
❌ checked exception
❌ wrong rollback configuration
Question 3
Can the database actually rollback?
Check:
❌ unsupported storage engine
❌ wrong datasource
❌ wrong transaction manager
Question 4
Am I using the expected transaction boundary?
Check:
❌ wrong propagation
❌ transaction placed at inappropriate layer
⭐ Interview shortcut
If interviewer asks:
“Why doesn’t
@Transactionalwork?”
Say:
“
@Transactionalworks through Spring AOP proxies. If the method call bypasses the proxy, the transaction interceptor isn’t executed. Common examples are self-invocation, creating the object withnew, and certain method visibility issues. Other causes include catching the exception, checked exceptions withoutrollbackFor, incorrect transaction-manager or datasource configuration, unsupported database transactions, and incorrect propagation settings.”
That’s a strong 2-minute interview answer.
If you want to really master this topic, the next thing to understand is Spring’s Proxy → AOP → TransactionInterceptor → PlatformTransactionManager → JDBC/Hibernate → Database flow. Once you understand that flow, almost every @Transactional problem becomes easy to debug.