Home/Learn/Hibernate & JPA/@Transactional Behaviour

@Transactional Behaviour

Intermediate
Transactions

@Transactional creates or joins a transaction on method entry and commits (or rolls back on unchecked exception) on exit; proxying means self-invocation bypasses the advice.

Overview

@Transactional is one of the most important annotations in any Spring JPA application. Placing it on a service method tells Spring to open a database transaction when the method is entered and to commit it when the method returns normally — or roll it back if an unchecked exception (RuntimeException or Error) propagates out. The annotation works through Spring AOP proxying: Spring wraps your bean in a CGLIB proxy, and the proxy intercepts the method call to manage the transaction. This has a critical implication — if you call a @Transactional method from within the same class (self-invocation), the proxy is bypassed and no transaction management happens. Understanding the exact rollback rules, the relationship between the persistence context and the transaction, and the self-invocation limitation is essential for writing correct JPA code.

Transaction & Persistence Context Lifecycle

When a @Transactional method begins, Spring binds a Hibernate Session (persistence context) to the current thread. The Session acts as the first-level cache and change-tracker. All repository operations within the method share this single Session. When the method returns normally, Spring calls session.flush() (writes pending SQL) and then commits the transaction. The Session closes and all managed entities become detached.

If a RuntimeException escapes, the transaction is rolled back and the Session is discarded — no SQL is written to the DB for that unit of work.

Java — @Transactional + JPA Session
@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepo;
    private final CustomerRepository customerRepo;

    @Transactional   // one Session, one transaction for this entire method
    public Order createOrder(Long customerId, OrderRequest req) {
        // Both queries share the same Session (first-level cache)
        Customer customer = customerRepo.findById(customerId).orElseThrow();
        Order order = new Order(customer, req);
        orderRepo.save(order);  // INSERT — queued in Session, not sent to DB yet

        customer.setOrderCount(customer.getOrderCount() + 1);
        // No explicit save() needed — dirty checking detects the change
        // customerRepo.save(customer)  ← NOT required, Hibernate handles this

        return order;
        // ON RETURN: flush() → SQL sent → transaction committed → Session closed
    }

    // Without @Transactional: each repository call opens+closes its own mini-transaction
    // Each call = separate Session → no dirty checking across calls, no shared cache
    public Order findOrder(Long id) {
        return orderRepo.findById(id).orElseThrow();
    }
}

Rollback Rules & readOnly

Default rollback: Spring rolls back on RuntimeException and Error. Checked exceptions (IOException, SQLException) do NOT trigger rollback by default — the transaction commits even if you catch a checked exception and rethrow a runtime one.

readOnly = true is a performance hint: Hibernate skips the dirty-checking flush before commit, and some DB drivers/replicas can route read-only transactions to a read replica. Always annotate read operations with readOnly = true.

Java — Rollback Rules
@Service
public class OrderService {

    // ✅ Read-only — skips dirty checking + flush, possible read-replica routing
    @Transactional(readOnly = true)
    public List<Order> getOrdersByCustomer(Long customerId) {
        return orderRepo.findByCustomerId(customerId);
    }

    // ✅ Roll back on checked exceptions too
    @Transactional(rollbackFor = Exception.class)
    public void importOrders(MultipartFile file) throws IOException {
        List<OrderRequest> orders = parseCSV(file.getInputStream()); // throws IOException
        orders.forEach(req -> orderRepo.save(new Order(req)));
        // IOException will now roll back the transaction
    }

    // ✅ Do NOT roll back on a specific business exception (e.g., log and continue)
    @Transactional(noRollbackFor = DuplicateOrderException.class)
    public void processOrder(OrderRequest req) {
        // DuplicateOrderException will NOT trigger rollback
    }
}

Self-Invocation — The Most Common Bug

@Transactional only works when called through the Spring proxy. Calling a @Transactional method from another method in the same bean calls the underlying target directly — no proxy, no transaction. This is the leading cause of "my transaction isn't working" bugs.

Java — Self-Invocation Fix
@Service
public class OrderService {

    // ❌ BROKEN — processOrder is called directly (no proxy), no transaction
    public void processBatch(List<OrderRequest> reqs) {
        reqs.forEach(req -> processOrder(req));  // self-invocation!
    }

    @Transactional
    public void processOrder(OrderRequest req) {
        orderRepo.save(new Order(req));
        // @Transactional has NO EFFECT when called from processBatch above
    }
}

// ✅ FIX — move processBatch to a separate bean
@Service
@RequiredArgsConstructor
public class OrderBatchService {

    private final OrderService orderService;  // injected proxy

    public void processBatch(List<OrderRequest> reqs) {
        reqs.forEach(req -> orderService.processOrder(req));  // goes through proxy ✅
    }
}

Key Points to Remember

  • 1@Transactional works via AOP proxy — self-invocation (calling a @Transactional method from the same class) completely bypasses transaction management.
  • 2Spring only rolls back on RuntimeException and Error by default; add rollbackFor = Exception.class for checked exceptions.
  • 3readOnly = true skips Hibernate dirty-checking and flush — always use it for read operations to improve performance.
  • 4One @Transactional method = one Hibernate Session = one persistence context = shared first-level cache for all queries in that method.
  • 5Dirty checking means you do not need to call save() on entities you modify inside a transaction — Hibernate detects and flushes changes automatically.
  • 6@Transactional on a private method has no effect — Spring proxies only intercept public method calls from external callers.

Interview Questions

Sign in to ask Aria
1

What is the self-invocation problem with @Transactional and how do you fix it?

HardThoughtworks
2

Does @Transactional roll back when a checked exception is thrown? How do you change this?

MediumAmazon
3

What is the relationship between a Spring @Transactional boundary and the Hibernate persistence context?

HardNetflix
4

What does @Transactional(readOnly = true) do at the Hibernate and DB level?

MediumGoogle
5

Why doesn't a @Transactional annotation on a private method work?

EasyInfosys

Ask Aria about @Transactional Behaviour

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…