Start Learning
Javaneer
Back to stage
Module 3·Data & Persistence

Transactions with @Transactional

Make multi-step changes all-or-nothing, understand propagation and rollback, and avoid the common pitfalls.

18 min readAdvanced
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.

A transaction is all-or-nothing@Transactional wraps these operations in one unit: they all commit together, or all roll back.
0BEGIN
1SELECT * FROM book WHERE isbn = ?
2UPDATE book SET copies_available = copies_available - 1
3INSERT INTO loan (book_id, member_id, ...)

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): @Transactional works through a proxy, so one method in a class calling another this.method() bypasses it - no transaction starts. Call across beans, or restructure.
  • Propagation: if borrow() calls another @Transactional method, 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.
A transaction is a bank transfer

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.

Why one annotation saves the data

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.
Was this lesson helpful?
Edit this page on GitHub