Home Spring @Transactional
SPRING

Spring @Transactional:
pitfalls that cause real production bugs

5 real-world cases, code examples, and straightforward explanations to avoid the most common mistakes.

8 min read 5 practical cases ▱ Keep handy
Context

An order and its audit log must be saved in the same transaction. process() is called from another Spring bean.

java
@Service 
public class OrderService { 

    public void process(Order order) { 
        saveOrder(order); // appel interne 
    } 

    @Transactional 
    public void saveOrder(Order order) { 
        orderRepository.save(order);
        auditRepository.save(
            new Audit(order.getId())
        );
    } 
}

? What happens with the @Transactional of saveOrder() ?

Explanation

In standard Spring proxy mode, @Transactional is applied when a call goes through the Spring proxy. Here, process() directly calls saveOrder() on the same instance. Therefore, the call does not go through the proxy, and the transaction declared on saveOrder() is not created.

!
Why is this dangerous?

Developers might assume that saving the order and the audit log forms a single atomic operation. In reality, this wrapping transaction does not exist in this call path. Spring Data repositories can run their own transactions: a first save might succeed even if a subsequent operation fails.

💡
Key Takeaway

A @Transactional placed on a method is not enough: the call must go through the Spring proxy. A common solution is to move the transactional method to another Spring bean so the call can be intercepted.

Context

By default, Spring does not trigger rollbacks in the same way for all exceptions.

java
@Transactional
public void process() throws Exception {
    repository.save(order);
    throw new Exception("Payment failed");
}

? What happens by default?

Explanation

By default, Spring automatically triggers a rollback for RuntimeException and Error. A checked exception like Exception does not automatically trigger this rollback.

!
Why is this dangerous?

A business error may be thrown even though the database changes were still committed.

💡
Key Takeaway

For example, use @Transactional(rollbackFor = Exception.class) when the expected behavior requires it.

Context

A private method directly holds @Transactional.

java
@Service
public class UserService {

    @Transactional
    private void saveUser() {
        repository.save(user);
    }
}

? Will the private method be intercepted?

Explanation

A private method cannot be intercepted by the Spring proxy mechanism. With a subclass-based proxy, a method final is also problematic since it cannot be overridden.

💡
Key Takeaway

For standard declarative transactions, prefer interceptable methods called through the proxy.

Context

A transactional method calls another method with Propagation.REQUIRES_NEW.

java
@Transactional
public void outer() {
    updateOrder();
    auditService.audit();
}

@Transactional(
    propagation = Propagation.REQUIRES_NEW
)
public void audit() {
    auditRepository.save(log);
}

? What is the expected behavior?

Explanation

REQUIRES_NEW starts a new independent physical transaction. The existing transaction is suspended during its execution.

!
Why is this important?

The inner transaction can be committed even if the outer transaction subsequently fails.

💡
Key Takeaway

This is useful for certain audits or isolated processing, but you need to understand the business consequences and the additional connection usage.

Context

An email is sent before the transaction finishes. An error occurs afterward.

java
@Transactional
public void register() {

    userRepository.save(user);

    emailService.sendWelcomeEmail(user);

    if (error) {
        throw new RuntimeException("fail");
    }
}

? What is the main issue?

Explanation

The database transaction can be rolled back, but an email already sent or an external call already made will not be automatically canceled.

!
Why is this dangerous?

The user might receive a confirmation even though the corresponding operation does not end up in the database.

💡
Key Takeaway

For critical external side effects, consider using post-commit events or an Outbox pattern when delivery guarantees need to be robust.

🏆

You completed all 5 pitfalls!

You now know how to avoid several common mistakes with @Transactional.

DevUpNow App
✓ 240+ technical resources ✓ Java, Spring, JPA, and PostgreSQL ✓ Interview mode ✓ Detailed explanations ✓ Personalized progress
Continue on DevUpNow
DEVUPNOW

Keep challenging yourself

240+ Java, Spring, JPA, and PostgreSQL questions with detailed explanations and interview mode.