# Transactions with Spring and JPA — Hibernate / JPA

Source: https://www.skillbyai.com/en/hibernate-jpa/t-tx

> 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.](assets/figures/hibernate-jpa/section-7-map.svg) — 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.

```java
@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.

**Quiz:** A checked exception escapes a Spring @Transactional method without rollbackFor. What happens by default?

- [x] 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.
