avatarVinotech

Summary

The provided content discusses the implementation and benefits of Optimistic Locking in JPA with Spring Boot to prevent data inconsistency in concurrent transactions.

Abstract

Optimistic Locking is a concurrency control mechanism in JPA that ensures data integrity by preventing multiple transactions from overwriting each other's changes. This is particularly important in enterprise applications where simultaneous read and write operations are common. The article explains how to implement Optimistic Locking in a Spring Boot application using the @Version annotation in the BankAccount entity. It demonstrates the process through a practical example involving simultaneous deposit requests, showing how the mechanism detects conflicts and raises an OptimisticLockException when a transaction attempts to update stale data. The article also provides guidance on handling such exceptions using a global exception handler. The example underscores the importance of Optimistic Locking in financial systems where maintaining accurate account balances is critical.

Opinions

  • The author emphasizes the importance of Optimistic Locking in scenarios where many read-and-write operations occur, suggesting it is a preferred approach in such cases.
  • The article suggests that Optimistic Locking is suitable for services with a low likelihood of version conflicts, as it allows for non-blocking read operations.
  • The author provides a subjective recommendation to use the @Version annotation in Spring Boot for implementing Optimistic Locking, implying it is an effective and straightforward solution.
  • By presenting a scenario with Alice and Sarah, the author illustrates the concept of stale data and the potential for data inconsistency without proper locking mechanisms.
  • The inclusion of a global exception handler for OptimisticLockException indicates the author's view that handling exceptions gracefully is a best practice in application development.
  • The author encourages readers to engage with the content by clapping and sharing it, indicating a desire for community feedback and recognition.
  • The article concludes by highlighting the critical nature of Optimistic Locking in financial systems, reinforcing the author's opinion on its significance in maintaining data integrity.

Optimistic Locking in JPA with Spring Boot

Optimistic Locking is a mechanism to ensure that multiple transactions do not overwrite each other’s changes. This is achieved by maintaining a version number in the entity, which is checked and updated during each transaction. If two transactions try to update the same entity concurrently, one will fail with an OptimisticLockException.

In enterprise applications, concurrent access to the database is crucial. Applications must perform transactions independently without locking, ensuring data integrity and consistency. Optimistic Locking allows two threads to read or modify the same data row without blocking each other. However, there is a problem to consider with this approach.

For instance, Alice has a bank account with a balance of $32,000. She wants to transfer $10,000 from a different account to hers using a mobile application. Simultaneously, Sarah realizes she forgot to pay rent for the month and decides to pay $1,000 using an ATM. Both submit their requests to the server simultaneously, Transaction A being created for Alice and Transaction B for Sarah.

Transaction A and Transaction B can modify data at the same time. Transaction B updates Alice’s account balance from $32,000 to $33,000 due to Sarah’s transaction. However, Alice still sees her balance as $32,000, assuming she updated it from $32,000 to $42,000. Is this correct? Hmm, not actually. Alice updated her balance from $33,000 to $43,000 because changes have not been yet committed by the transaction. This phenomenon is known as Stale Data.

We would expect Alice to update her bank account balance from $32,000 to $42,000 because she deposited $10,000. However, since Sarah also deposited $1,000 at the same time, Alice’s balance actually updated from $32,000 to $33,000. Later, when Alice checks her balance, she assumes it was updated from $32,000 to $42,000, but in reality, it was updated from $33,000 to $43,000 due to Sarah’s transaction

How to solve this problem. We can prevent the second Transaction (user) from updating stale data. We can solve this problem by adding the version column. Optimistic Locking detects changes on the data by checking the version column. It is suitable If the service has many read-and-write operations. We can provide this using @Version annotation in Spring Boot.

Let’s create an example of Optimistic Locking using a BankAccount entity, where multiple requests try to update the account balance simultaneously.

1. Create a Spring Boot Project

Use Spring Initializr to create a new Spring Boot project with the following dependencies:

  • Spring Web
  • Spring Data JPA
  • H2 Database (for simplicity)
  • Lombok (optional)

2. Define the BankAccount Entity

The BankAccount entity will have a @Version field to implement optimistic locking.

package com.example.demo.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Version;
import java.math.BigDecimal;

@Entity
public class BankAccount {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String accountHolderName;
    private BigDecimal balance;
    
    @Version
    private int version;

    // Constructors, Getters, and Setters
    public BankAccount() {}

    public BankAccount(String accountHolderName, BigDecimal balance) {
        this.accountHolderName = accountHolderName;
        this.balance = balance;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getAccountHolderName() {
        return accountHolderName;
    }

    public void setAccountHolderName(String accountHolderName) {
        this.accountHolderName = accountHolderName;
    }

    public BigDecimal getBalance() {
        return balance;
    }

    public void setBalance(BigDecimal balance) {
        this.balance = balance;
    }

    public int getVersion() {
        return version;
    }

    public void setVersion(int version) {
        this.version = version;
    }
}

3. Create the BankAccountRepository

package com.example.demo.repository;

import com.example.demo.entity.BankAccount;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface BankAccountRepository extends JpaRepository<BankAccount, Long> {
}

4. Create the BankAccountService

package com.example.demo.service;

import com.example.demo.entity.BankAccount;
import com.example.demo.repository.BankAccountRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Service
public class BankAccountService {

    @Autowired
    private BankAccountRepository bankAccountRepository;

    @Transactional
    public BankAccount deposit(Long accountId, BigDecimal amount) {
        BankAccount bankAccount = bankAccountRepository.findById(accountId)
            .orElseThrow(() -> new RuntimeException("Bank account not found"));

        bankAccount.setBalance(bankAccount.getBalance().add(amount));
        return bankAccountRepository.save(bankAccount);
    }
}

5. Create the BankAccountController

package com.example.demo.controller;

import com.example.demo.entity.BankAccount;
import com.example.demo.service.BankAccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.math.BigDecimal;

@RestController
@RequestMapping("/accounts")
public class BankAccountController {

    @Autowired
    private BankAccountService bankAccountService;

    @PutMapping("/{id}/deposit")
    public BankAccount deposit(@PathVariable Long id, @RequestParam BigDecimal amount) {
        return bankAccountService.deposit(id, amount);
    }
}

6. Simulate Simultaneous Requests

Let’s simulate two deposit requests coming in at the same time:

  1. Initial State: Assume the BankAccount entity with id = 1 has the following data:
  • accountHolderName: "Alice"
  • balance: 1000.00
  • version: 0

2. Request 1:

  • Input: Deposit 500.00.
  • Endpoint: PUT /accounts/1/deposit?amount=500.00

3. Request 2 (comes in just after Request 1, but before Request 1 completes):

  • Input: Deposit 300.00.
  • Endpoint: PUT /accounts/1/deposit?amount=300.00

4. Outcome:

  • Request 1 updates the BankAccount entity, and the balance becomes 1500.00, with version incremented to 1.
  • Request 2 attempts to update the BankAccount entity. However, it fails with an OptimisticLockException because the version it read (0) does not match the current version (1).

7. Sample Input and Output

Initial Data in the Database:

INSERT INTO BANK_ACCOUNT (id, account_holder_name, balance, version) VALUES (1, 'Alice', 1000.00, 0);

Request 1 Input:

PUT /accounts/1/deposit?amount=500.00

Request 1 Output:

{
    "id": 1,
    "accountHolderName": "Alice",
    "balance": 1500.00,
    "version": 1
}

Request 2 Input:

PUT /accounts/1/deposit?amount=300.00

Request 2 Output (Exception):

{
    "timestamp": "2024-09-01T12:34:56.789+00:00",
    "status": 409,
    "error": "Conflict",
    "message": "Optimistic lock exception occurred",
    "path": "/accounts/1/deposit"
}

8. Handling OptimisticLockException To handle the exception, you can create a global exception handler:

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.persistence.OptimisticLockException;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(OptimisticLockException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public String handleOptimisticLockException(OptimisticLockException e) {
        return "Optimistic lock exception occurred: " + e.getMessage();
    }
}

Summary

In this example, we’ve implemented optimistic locking in a Spring Boot application using JPA’s @Version annotation with a BankAccount entity. We demonstrated how two simultaneous deposit requests could lead to an OptimisticLockException. This scenario is particularly relevant in financial systems where consistent account balances are critical.

👏 If you found my articles useful, please consider giving it claps and sharing it with your friends and colleagues.

Optimistic Locking
Database Transaction
Spring Data Jpa
Concurrency
Locking Techniques
Recommended from ReadMedium