Why @Transactional Is Not Working in Spring Boot — 10 Common Reasons

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:

  1. Give one biscuit to Tom.
  2. 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 @Transactional work?”

Say:

@Transactional works 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 with new, and certain method visibility issues. Other causes include catching the exception, checked exceptions without rollbackFor, 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.

Leave a Reply