Transactions with @Transactional
Make multi-step changes all-or-nothing, understand propagation and rollback, and avoid the common pitfalls.
On this page
Borrowing a book in BookVault is two changes that must happen together: decrement the book's available copies and create a loan record. If the first succeeds and the second fails, you've reserved a copy for a loan that doesn't exist - corrupt data. A transaction makes the pair all-or-nothing.
@Transactional
Annotate the method that must be atomic:
@Service
class LendingService {
@Transactional
public Loan borrow(String isbn, Long memberId) {
Book book = bookRepository.findByIsbn(isbn).orElseThrow(...);
if (book.getCopies() < 1) throw new NoCopiesAvailableException(isbn);
book.setCopies(book.getCopies() - 1); // change 1
return loanRepository.save(new Loan(book, memberId)); // change 2
}
}Spring wraps the method in a database transaction: it commits if the method returns normally, and rolls back if it throws. Either both changes land, or neither does.
Rollback rules
By default, Spring rolls back on unchecked exceptions (RuntimeException and its
subclasses) and commits otherwise. Your domain exceptions - like
NoCopiesAvailableException - should extend RuntimeException, so a business failure
automatically undoes the transaction.
Read-only for pure reads
Mark query-only methods @Transactional(readOnly = true). It lets the database and
Hibernate optimize (no dirty-checking, possible replica routing) and documents intent:
this method changes nothing.
Two pitfalls to remember
- Self-invocation (from the AOP lesson):
@Transactionalworks through a proxy, so one method in a class calling anotherthis.method()bypasses it - no transaction starts. Call across beans, or restructure. - Propagation: if
borrow()calls another@Transactionalmethod, the default (REQUIRED) joins the same transaction - so an inner failure rolls back the whole thing. You rarely need to change this, but know it exists.
Moving money between accounts is a debit and a credit. If the debit happens but the
credit fails, money vanishes. Banks make the pair a single transaction: both post,
or neither does, and the books always balance. @Transactional gives your borrow-a-book
operation the same guarantee - the copy count and the loan record can never disagree.
In BookVault's borrow method, the copy-count decrement succeeds but the loan
INSERT fails (say, a constraint violation). Explain what @Transactional does at that
moment, and what the catalog looks like afterward compared with if the method had no
transaction.
What does @Transactional do when a method throws a RuntimeException partway through?
Key takeaways
- @Transactional makes a method's multiple database changes atomic: all commit together on success, or all roll back on failure.
- By default Spring rolls back on unchecked (RuntimeException) exceptions; make domain exceptions unchecked so business failures undo the transaction.
- Use @Transactional(readOnly = true) for query-only methods to optimize and document intent.
- Self-invocation bypasses the proxy, so an internal this.method() call won't start a transaction.
- Default propagation REQUIRED joins an existing transaction, so an inner failure rolls back the whole operation.