Java > Spring Framework > Spring Data > Spring Transactions
Declarative Transaction Management with Spring Data JPA
This snippet demonstrates declarative transaction management in a Spring Boot application using Spring Data JPA. It covers defining a service method to transfer funds between two accounts and handling potential exceptions within a transactional context.
Core Concepts
Spring's declarative transaction management simplifies transaction handling by using annotations like @Transactional
. This annotation demarcates a method as transactional, allowing Spring to automatically handle transaction boundaries (begin, commit, rollback). Spring Data JPA provides repositories that abstract away boilerplate database interaction code.
Entity Definitions (Account)
This defines a simple Account
entity with fields like id
, accountNumber
, and balance
. The @Entity
annotation marks this class as a JPA entity, and @Id
and @GeneratedValue
define the primary key.
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String accountNumber;
private double balance;
public Account() {}
public Account(String accountNumber, double balance) {
this.accountNumber = accountNumber;
this.balance = balance;
}
// Getters and setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getAccountNumber() {
return accountNumber;
}
public void setAccountNumber(String accountNumber) {
this.accountNumber = accountNumber;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
Repository Interface (AccountRepository)
This interface extends JpaRepository
, providing basic CRUD operations for the Account
entity. The findByAccountNumber
method is a custom query method that Spring Data JPA will automatically implement based on the method name.
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
Account findByAccountNumber(String accountNumber);
}
Service Layer with Transaction Management
The BankService
class handles the fund transfer logic. The @Transactional
annotation on the transferFunds
method ensures that the entire operation (reading, updating, and saving account balances) is executed within a single transaction. If any exception occurs during the process, the transaction will be rolled back, ensuring data consistency. The custom exception `InsufficientFundsException` extends `RuntimeException` so that the transaction is rolled back when it occurs. Otherwise, if it extended `Exception`, Spring will not automatically roll back the transaction unless explicitly configured to do so.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BankService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void transferFunds(String fromAccountNumber, String toAccountNumber, double amount) {
Account fromAccount = accountRepository.findByAccountNumber(fromAccountNumber);
Account toAccount = accountRepository.findByAccountNumber(toAccountNumber);
if (fromAccount == null || toAccount == null) {
throw new IllegalArgumentException("Invalid account numbers.");
}
if (fromAccount.getBalance() < amount) {
throw new InsufficientFundsException("Insufficient funds.");
}
fromAccount.setBalance(fromAccount.getBalance() - amount);
toAccount.setBalance(toAccount.getBalance() + amount);
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
}
}
@SuppressWarnings("serial")
class InsufficientFundsException extends RuntimeException {
public InsufficientFundsException(String message) {
super(message);
}
}
Real-Life Use Case
This pattern is commonly used in banking applications for transferring funds between accounts, e-commerce platforms for processing payments, and any system that requires maintaining data consistency across multiple operations.
Best Practices
Interview Tip
Be prepared to discuss different transaction isolation levels (READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE) and their impact on data consistency and concurrency. Also, understand the propagation behavior of transactions (REQUIRED, REQUIRES_NEW, SUPPORTS).
When to Use Them
Use declarative transaction management when you need to ensure ACID properties (Atomicity, Consistency, Isolation, Durability) for a series of database operations. It's particularly useful when dealing with complex business logic involving multiple database interactions.
Alternatives
TransactionTemplate
or PlatformTransactionManager
. This approach offers more flexibility but requires more code.
Pros
Cons
FAQ
-
What happens if an exception is thrown outside the
@Transactional
method?
If an exception is thrown outside the@Transactional
method, it will not affect the transaction. Only exceptions thrown within the transactional context can trigger a rollback, and only if the exception type is configured to trigger rollback (by default, RuntimeExceptions and Errors). -
How do I configure different transaction isolation levels?
You can configure the transaction isolation level using theisolation
attribute of the@Transactional
annotation. For example:@Transactional(isolation = Isolation.READ_COMMITTED)
. -
What is the default propagation behavior of
@Transactional
?
The default propagation behavior isPropagation.REQUIRED
. This means that if a transaction already exists, the method will join the existing transaction. If no transaction exists, a new transaction will be created.