Pessimistic Locking in JPA with Spring Boot
Understanding Pessimistic Locking
Before diving into Spring Boot implementations, let’s establish a solid understanding of pessimistic locking and its significance in database transactions. Pessimistic locking is a concurrency control mechanism where a lock is acquired on a database record or resource before performing any modifications. This lock prevents other transactions from accessing or modifying the locked resource until the lock is released, thereby ensuring data consistency and preventing conflicts in concurrent environments.
Pessimistic locking ensures that a database row is locked for the duration of a transaction to prevent other transactions from modifying it. When two requests try to update the same record simultaneously, one will wait until the other completes.
Implementing Pessimistic Locking in Spring Boot
Spring Boot provides support for implementing pessimistic locking through its integration with JPA (Java Persistence API) and the underlying database. Let’s explore how to implement pessimistic locking in Spring Boot using various strategies and techniques.
1. Pessimistic Write Lock
Pessimistic write lock, also known as “SELECT … FOR UPDATE,” is a common approach to acquiring a lock on database records for write operations. In Spring Boot, you can leverage JPA’s LockModeType to acquire a pessimistic write lock on entities.
2. Pessimistic Read Lock
Pessimistic read lock allows multiple transactions to read the same data concurrently while preventing any transaction from modifying the locked data. This can be useful for scenarios where data consistency is critical, but read operations are frequent.
Example Scenario
Let’s consider an Account entity where two users try to withdraw money from the same account at the same time. We will use Pessimistic Locking to ensure that only one user can perform the withdrawal at a time.
1. Maven Dependencies
Ensure you have the following dependencies in your pom.xml:
<dependencies>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 Database for testing -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>2. Account Entity
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.Lock;
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String accountNumber;
private Double balance;
@Version
private Integer version;
// Getters and Setters
}3. AccountRepository
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.stereotype.Repository;
import jakarta.persistence.LockModeType;
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
Account findByAccountNumber(String accountNumber);
}4. AccountService
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void withdraw(String accountNumber, Double amount) {
Account account = accountRepository.findByAccountNumber(accountNumber);
if (account.getBalance() >= amount) {
account.setBalance(account.getBalance() - amount);
accountRepository.save(account);
} else {
throw new RuntimeException("Insufficient balance");
}
}
}5. AccountController
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/accounts")
public class AccountController {
@Autowired
private AccountService accountService;
@PostMapping("/withdraw")
public String withdraw(@RequestBody WithdrawRequest request) {
accountService.withdraw(request.getAccountNumber(), request.getAmount());
return "Withdrawal successful";
}
}
class WithdrawRequest {
private String accountNumber;
private Double amount;
// Getters and Setters
}6. Testing Simultaneous Requests
To test this, you can create two threads simulating simultaneous requests.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class TestRunner implements CommandLineRunner {
@Autowired
private AccountService accountService;
@Override
public void run(String... args) throws Exception {
// Create an account with a balance
Account account = new Account();
account.setAccountNumber("12345");
account.setBalance(1000.0);
accountRepository.save(account);
// Simulate two users trying to withdraw money at the same time
Runnable task1 = () -> accountService.withdraw("12345", 800.0);
Runnable task2 = () -> accountService.withdraw("12345", 300.0);
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}7. Sample Input and Output
Initial State:
Account 12345withBalance = 1000.0
Simultaneous Requests:
Thread 1: Withdraw800.0Thread 2: Withdraw300.0
Expected Output:
- Thread 1 successfully withdraws
800.0, leaving a balance of200.0. - Thread 2 fails with “Insufficient balance” because the account balance was reduced to
200.0by Thread 1.
Console Output:
Withdrawal successful (Thread 1)
Exception in thread "Thread-2" java.lang.RuntimeException: Insufficient balanceExplanation
When both threads attempt to withdraw from the account, the first one that acquires the lock (Thread 1) processes its transaction. Thread 2 will be blocked until Thread 1 completes. When Thread 2 resumes, it finds that the balance is insufficient and throws an exception.
This example shows how Pessimistic Locking ensures data integrity by preventing simultaneous modifications to the same record.
👏 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





