Spring Boot HandBook

    Transactions in Spring#

    Introduction#

    In any real-world application, especially when interacting with databases, managing data integrity is crucial. Transactions allow us to ensure that a group of operations are executed in an "all-or-nothing" fashion. This means that if one operation fails, all changes made by other operations in the transaction should be rolled back, leaving the system in a consistent state.

    What is a Transaction?#

    A transaction is a sequence of operations that are treated as a single unit of work. These operations either succeed together or fail together. For example, in a banking system, transferring money from one account to another involves debiting one account and crediting another. Both operations must succeed, or neither should be completed.

    Why Do We Need Transactions?#

    Without transactions, partial operations could leave the system in an inconsistent state, resulting in data corruption or loss. Transactions ensure:

    • Atomicity: All operations in a transaction are completed successfully, or none at all.
    • Consistency: The system remains in a valid state before and after the transaction.
    • Isolation: Transactions occur independently, without interference.
    • Durability: Once a transaction is committed, the changes are permanent, even if the system crashes.

    @Transactional Annotation in Spring#

    Spring provides the @Transactional annotation to handle transactions declaratively. When a method is annotated with @Transactional, Spring manages the transaction automatically.

    How Does @Transactional Work?#

    • Begin Transaction: When the annotated method is called, Spring opens a new transaction.
    • Commit or Rollback: If the method executes successfully, the transaction is committed, and changes are saved. If an exception occurs, the transaction is rolled back, and changes are discarded.

    Example: Using @Transactional#

    Let's walk through a simple example of managing a transaction in a service layer.

    Entity Classes#

    We have two entities, Account and TransactionRecord. When we transfer money, both entities need to be updated.

    @Entity public class Account { @Id private Long id; private String accountNumber; private Double balance; // Getters and Setters }
    @Entity public class TransactionRecord { @Id private Long id; private String transactionType; private Double amount; private Date transactionDate; // Getters and Setters }

    Service Class with Transaction Management#

    @Service public class BankService { @Autowired private AccountRepository accountRepository; @Autowired private TransactionRecordRepository transactionRecordRepository; @Transactional public void transferMoney(String fromAccount, String toAccount, Double amount) { Account sender = accountRepository.findByAccountNumber(fromAccount); Account receiver = accountRepository.findByAccountNumber(toAccount); // Debit from sender sender.setBalance(sender.getBalance() - amount); accountRepository.save(sender); // Credit to receiver receiver.setBalance(receiver.getBalance() + amount); accountRepository.save(receiver); // Record the transaction TransactionRecord record = new TransactionRecord(); record.setTransactionType("Transfer"); record.setAmount(amount); record.setTransactionDate(new Date()); transactionRecordRepository.save(record); } }

    Explanation:#

    • The @Transactional annotation ensures that all operations within the transferMoney method (debit, credit, and recording the transaction) are executed within a single transaction.
    • If any operation fails (e.g., saving the transaction record), the entire transaction is rolled back, leaving the accounts unchanged.

    Transaction Propagation and Isolation Levels#

    • Propagation: Defines how transactions behave in nested method calls. For example, Propagation.REQUIRED means that the method should run within a transaction; if one doesn't exist, a new transaction is started.
    • Isolation Levels: Control how changes made by one transaction are visible to other transactions. Spring provides various isolation levels like READ_COMMITTED, REPEATABLE_READ, and SERIALIZABLE.

    Dirty Read - A Dirty read is a situation when a transaction reads data that has not yet been committed.

    Non Rpeatable Read - Non Repeatable read occurs when a transaction reads the same row twice and gets a different value each time.

    Phantom Read - Phantom Read occurs when two same queries are executed, but the rows retrieved by the two, are different.

    Key Points#

    1. Use @Transactional at the service layer where business logic and database operations are managed.
    2. By default, @Transactional rolls back on RuntimeExceptions or unchecked exceptions.
    3. Transactions ensure data consistency and integrity, making them crucial in systems that handle financial operations, order processing, and more.

    In this article, we examined the importance of transactions in Spring and how they ensure data integrity in applications interacting with databases. We defined transactions as sequences of operations that are treated as a single unit of work, emphasizing the need for atomicity, consistency, isolation, and durability (ACID properties).

    Last updated on Oct 25, 2024