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.
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
- 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).
- 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 toUnexpectedRollbackExceptionat commit. - 03
Self-invocation: calling
this.save()from another method of the same bean skips the proxy, so@Transactionalon save is ignored. Move the method to another bean, or put the annotation on the entry point. - 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. - 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.@Transactionalon private methods does nothing with proxy-based AOP.
Code & diagrams
@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
What is the difference between REQUIRES_NEW and NESTED?
Practice
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.