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:
- Initial State: Assume the
BankAccountentity withid = 1has the following data:
accountHolderName: "Alice"balance:1000.00version: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
BankAccountentity, and thebalancebecomes1500.00, withversionincremented to1. - Request 2 attempts to update the
BankAccountentity. However, it fails with anOptimisticLockExceptionbecause theversionit read (0) does not match the currentversion(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.
- Mastering Transaction Propagation and Isolation in Spring Boot
- @Formula Annotation in Spring Boot
- SOLID Principles in Java
- Java String Interview Questions and answer
- Design Pattern in java
- Kafka Interview Questions and Answers
- Java 8 Interview Questions and Answer
- Global exception handling in spring boot
- Pessimistic Locking in JPA with Spring Boot
- Optimistic Locking in JPA with Spring Boot
- Generic ApiResponse and Global Exception Handling in Spring Boot
- One To One mapping in Spring Boot JPA
- One To Many mapping in Spring Boot JPA




