Keep your data consistent — wrap multi-step work in @Transactional so it all commits or all rolls back, and control rollback rules, propagation, and read-only access.
Why: a transaction makes several writes atomic — they all succeed or all roll back, so you never leave half-finished data. When: any service method that does more than one write (place an order: save it, decrement stock). Note: put it on the service method; Spring wraps the call in a transaction.
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
@Transactional // commit at the end, or roll back
public Order placeOrder(Long bookId, int qty) {
Book book = books.findById(bookId).orElseThrow();
book.decreaseStock(qty); // write 1
return orders.save(new Order(book, qty)); // write 2
// if anything throws, BOTH writes roll back
}
}Note: by default Spring rolls back on unchecked exceptions (RuntimeException) but commits on checked exceptions — a classic surprise. When rollbackFor: force a rollback on a checked exception too. Catching an exception inside the method silently commits, so let it propagate.
@Transactional(rollbackFor = Exception.class) // roll back on ANY exception
public void importBooks(List<Book> batch) throws IOException {
for (Book b : batch) {
books.save(b);
if (b.getPrice() < 0) throw new IOException("bad price");
// ↑ checked exception now triggers a rollback of the whole batch
}
}Why: propagation decides what happens when a transactional method calls another. When REQUIRED (default): join the caller’s transaction if one exists. When REQUIRES_NEW: suspend the caller’s and run in a fresh, independent transaction — useful for audit logs that must persist even if the outer work rolls back.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeAuditLog(String message) {
// commits independently — survives even if the caller rolls back
auditRepository.save(new AuditEntry(message));
}When readOnly = true: a method that only queries. Note: it lets Hibernate skip dirty-checking (no change tracking) and hints the database/driver to optimize, giving cleaner, slightly faster reads. A good default on query-only service methods.
@Transactional(readOnly = true)
public List<Book> catalog() {
return books.findAll(); // no writes — skip change tracking
}