Spring Boot: From Basics to Production · Persistence with Spring Data JPA

Transactions with @Transactional

Define transaction boundaries and avoid the common proxy pitfalls.

@Transactional on service methods opens a transaction, commits on success and rolls back on unchecked exceptions (configure rollbackFor for checked ones). Use readOnly = true for queries. It works through proxies, so self-invocation (calling a @Transactional method from the same class) bypasses it, and it only applies to public methods called from outside. Keep transactions short and never call slow external services inside them.

@Service
public class TransferService {
    private final AccountRepository accounts;
    TransferService(AccountRepository accounts) { this.accounts = accounts; }

    @Transactional
    public void transfer(long fromId, long toId, long cents) {
        Account from = accounts.findById(fromId).orElseThrow();
        Account to = accounts.findById(toId).orElseThrow();
        from.withdraw(cents);       // throws InsufficientFunds (RuntimeException) → rollback
        to.deposit(cents);
        // no save() needed: managed entities are flushed on commit (dirty checking)
    }

    @Transactional(readOnly = true)
    public AccountView view(long id) {
        return accounts.findViewById(id).orElseThrow();
    }
}

Common pitfalls

  • Self-invocation: this.transfer() from another method in the same bean skips the proxy.
  • Checked exceptions don't trigger rollback by default.
  • HTTP calls or email sending inside transactions.

Key points

  • Service-level transaction boundaries
  • Proxy-based: watch self-invocation
  • Rollback on unchecked exceptions by default

Further reading: Spring: Declarative transactions

Practice questions

Why doesn't @Transactional work when a method calls another @Transactional method in the same class?

Answer

Transactions are applied by a proxy around the bean; internal calls via this don't go through the proxy, so the annotation on the called method is ignored.

Why don't you need to call save() after modifying a loaded entity?

Answer

Entities loaded in a transaction are managed; Hibernate's dirty checking flushes changes automatically at commit.

Open in the app to mark lessons complete, take notes and track your progress.