पाठ 19 / 25

Transactions with Spring and JPA

Use @Transactional correctly: boundaries, propagation, rollback rules and proxy pitfalls.

Where a unit of work begins and ends

Every JPA write must happen inside a transaction, and reads should too. In Spring, annotate service methods with @Transactional: Spring's proxy begins a transaction before the method, binds an EntityManager to it, and commits (flushing changes) when the method returns, or rolls back on failure. Put transactions at the service layer, around one business operation, not in controllers or on every repository call separately. Rollback rules: by default Spring rolls back for unchecked exceptions (RuntimeException, Error) but commits for checked exceptions unless you set rollbackFor. Propagation controls nesting: REQUIRED (default, join an existing transaction or start one), REQUIRES_NEW (suspend the current one and start an independent one, useful for audit logs that must survive a rollback), MANDATORY, SUPPORTS and others. A notorious pitfall: self-invocation. Calling a @Transactional method from another method in the same class bypasses the proxy, so the annotation has no effect. Annotations on private methods are also ignored by proxy-based configurations.

A transactional proxy

Calls from outside go through the proxy, which opens and commits the transaction; internal calls skip it.

A wrapper box around an inner service box; one arrow enters the wrapper from outside, another loops from inside the service back to itself without touching the wrapper.
Figure 7.1 — Spring's transactional proxy and the self-invocation pitfall.

Transaction boundaries, rollback rules and self-invocation

The audit log commits independently; the internal call misses the proxy.

@Service
public class CheckoutService {
    private final OrderRepository orders;
    private final AuditService audit;            // separate bean: proxy applies

    public CheckoutService(OrderRepository orders, AuditService audit) {
        this.orders = orders; this.audit = audit;
    }

    @Transactional(rollbackFor = PaymentDeclinedException.class)   // checked exception -> rollback
    public Long checkout(Cart cart) throws PaymentDeclinedException {
        PurchaseOrder order = PurchaseOrder.from(cart);
        orders.save(order);
        audit.record("checkout attempted", order.getId());   // REQUIRES_NEW: survives rollback
        chargeCard(order);                                   // may throw PaymentDeclinedException
        return order.getId();
    }

    public void retryLater(Long orderId) {
        markForRetry(orderId);                 // SELF-INVOCATION: @Transactional below is ignored
    }

    @Transactional
    public void markForRetry(Long orderId) { /* runs without a transaction when called above */ }
}

@Service
class AuditService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void record(String event, Long orderId) { /* insert audit row */ }
}

Keep transactions short

Do not call slow external APIs or wait for user input inside a database transaction. Long transactions hold locks and connections; gather data first, then open a short transaction to write.

त्वरित जाँच: A checked exception escapes a Spring @Transactional method without rollbackFor. What happens by default?

  • The transaction is committed
  • The transaction is rolled back
  • Spring retries the method
  • The application stops
Answer

The transaction is committed — By default only unchecked exceptions and errors trigger rollback.