Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~8 min· topic 5 of 5

Topic 8.5

Spring Transactions: Propagation, Read-Only, Rollback and Pitfalls

In one line

@Transactional wraps a method in a database transaction via a proxy. Propagation decides whether to join an existing transaction (REQUIRED), suspend it and start a new one (REQUIRES_NEW), or use a savepoint (NESTED). Rollback happens on unchecked exceptions by default. Self-invocation bypasses the proxy, and long transactions hold connections and locks.

0/5 · 0%

Think of it like this

A travel agent booking a trip. By default everything is one booking that is confirmed or cancelled together (REQUIRED). Some steps, like writing to the complaints log, must be kept even if the trip is cancelled (REQUIRES_NEW).

Key ideas

  1. 01

    REQUIRED (default): join the caller's transaction or start one. REQUIRES_NEW: suspend the outer transaction and use a new one, with a second connection, committed independently (audit logs, outbox claims). It can deadlock or starve the pool if the outer transaction holds locks the inner needs. NESTED: a savepoint in the same transaction (JDBC only).

  2. 02

    Rollback rules: rolls back on RuntimeException and Error; commits on checked exceptions unless rollbackFor = Exception.class. Catching an exception inside a REQUIRED inner method that already marked the transaction rollback-only leads to UnexpectedRollbackException at commit.

  3. 03

    Self-invocation: calling this.save() from another method of the same bean skips the proxy, so @Transactional on save is ignored. Move the method to another bean, or put the annotation on the entry point.

  4. 04

    readOnly = true: hints Hibernate to skip dirty checking (flush mode MANUAL), can route to replicas with a routing DataSource, and sets the connection read-only.

  5. 05

    Keep transactions short: no HTTP calls, message sends or file uploads inside. Isolation can be set per method (isolation = Isolation.SERIALIZABLE) but needs retry logic. @Transactional on private methods does nothing with proxy-based AOP.

Code & diagrams

OrderService.javajava
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orders;
    private final StockRepository stock;
    private final OutboxRepository outbox;
    private final AuditService audit;

    @Transactional                                   // REQUIRED, rolls back on RuntimeException
    public Order place(PlaceOrder cmd) {
        int updated = stock.reserve(cmd.productId(), cmd.qty());   // UPDATE ... WHERE qty >= ?
        if (updated == 0) throw new OutOfStockException();
        Order o = orders.save(Order.from(cmd));
        outbox.save(OutboxEvent.orderPlaced(o));      // same transaction as the order
        audit.record("order.placed", o.getId());      // REQUIRES_NEW, see below
        return o;
    }

    @Transactional(readOnly = true)
    public List<OrderRow> history(long customerId) { return orders.rowsForCustomer(customerId); }
}

@Service
class AuditService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)   // survives outer rollback, uses a 2nd connection
    public void record(String action, long id) { /* insert audit row */ }
}

Interview problem

The problem

Why didn't this roll back?

placeOrder() (not transactional) calls this.saveOrderAndItems() annotated @Transactional. When the third item insert fails, the order and first two items remain in the database. A second service catches DataIntegrityViolationException inside a transactional method and returns normally, yet the caller gets UnexpectedRollbackException. Explain both.

When it breaks

REQUIRES_NEW inside a loop under load

What you see

Each outer transaction holds one connection and needs a second for the inner; with the pool exhausted, all threads wait for each other: pool deadlock.

Fix & prevent

Avoid REQUIRES_NEW on hot paths; if needed, size the pool for 2 connections per concurrent request or do the work after commit.

Explain it without notes

01

What is the difference between REQUIRES_NEW and NESTED?

Practice

01

Make a service method roll back on a checked PaymentDeclinedException.

Trade-offs

  • ↔

    Declarative transactions are concise but proxy-based rules (self-invocation, visibility, rollback defaults) cause silent bugs.

Done when you can

  • I can choose propagation and rollback rules and avoid self-invocation and long-transaction pitfalls.