Ensuring thread safety with Optimistic and Pessimistic Locking
When dealing with multi-user applications in Spring Boot, ensuring thread safety is crucial to avoid race conditions, data corruption, and unexpected behaviour. In this blog post, I will explain how to make a Spring Boot function thread-safe using Optimistic Locking and Pessimistic Locking using ShiftSL, our hospital roster management system.
A common issue arises when multiple users attempt to claim the same shift in the roster management system.
The Problem: Race Condition in Shift Claiming
Consider the following function that allows doctors to claim available shifts:
@Transactional
public void claimShift(Long doctorId, Long shiftId) {
try {
Shift shift = getShiftByID(shiftId);
User user = userService.getUserById(doctorId);
if (!shift.isShiftAvailable()) {
throw new DoctorCountExceededException("Number of assigned doctors exceeds the allowed limit.");
}
Set<User> doctors = shift.getDoctors();
doctors.add(user);
shift.setDoctors(doctors);
shift.setShiftAvailable(doctors.size() < shift.getNoOfDoctors());
shiftRepo.save(shift);
}
// exception handelling part
What Could Go Wrong?
If two doctors try to claim the same shift simultaneously, they might both read the shift as available, add themselves, and save the shift, resulting in one doctor being overwritten.
Solution 1: Optimistic Locking
Optimistic Locking prevents lost updates by detecting concurrent modifications. It allows multiple users to read data but throws an exception if two updates happen simultaneously.
Step 1: Add @Version to the Shift Entity
@Entity
public class Shift {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version // Enables Optimistic Locking
private int version;
private boolean shiftAvailable;
private int noOfDoctors;
@ManyToMany
private Set<User> doctors;
// Getters and Setters
}
Step 2: Modify the claimShift Method to Handle Version Conflicts
@Transactional
public void claimShift(Long doctorId, Long shiftId) {
try {
Shift shift = getShiftByID(shiftId);
User user = userService.getUserById(doctorId);
if (!shift.isShiftAvailable()) {
throw new DoctorCountExceededException("Number of assigned doctors exceeds the allowed limit.");
}
shift.getDoctors().add(user);
shift.setShiftAvailable(shift.getDoctors().size() < shift.getNoOfDoctors());
shiftRepo.save(shift);
} catch (OptimisticLockException e) {
throw new ConcurrencyFailureException("Shift has been modified by another transaction. Please try again.", e);
}
// exception handelling part
}
Why Optimistic Locking?
No blocking — allows concurrent reads Works well for applications with low contention
But If conflicts occur frequently, users will see retry errors
Solution 2: Pessimistic Locking
If multiple users frequently claim shifts simultaneously, Pessimistic Locking ensures that only one transaction at a time can update the shift.
Step 1: Modify the Repository to Use Pessimistic Locking
@Repository
public interface ShiftRepository extends JpaRepository<Shift, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT s FROM Shift s WHERE s.id = :shiftId")
Shift findShiftWithLock(@Param("shiftId") Long shiftId);
}
Step 2: Update the claimShift Method to Use Locked Queries
@Transactional
public void claimShift(Long doctorId, Long shiftId) {
try {
Shift shift = shiftRepo.findShiftWithLock(shiftId); // Prevents concurrent updates
User user = userService.getUserById(doctorId);
if (!shift.isShiftAvailable()) {
throw new DoctorCountExceededException("Number of assigned doctors exceeds the allowed limit.");
}
shift.getDoctors().add(user);
shift.setShiftAvailable(shift.getDoctors().size() < shift.getNoOfDoctors());
shiftRepo.save(shift);
}
// exception handelling part
}
Why Pessimistic Locking?
Prevents race conditions completely Guarantees data consistency
But Slower performance due to row-level locking
Conclusion
Scenario Best Approach
Low contention (few concurrent updates) — Optimistic Locking
High contention (many concurrent updates) & Need to prevent modifications during processing — Pessimistic Locking
Both approaches improve thread safety in a multi-user system. Choose Optimistic Locking for better performance in most cases, and use Pessimistic Locking when strict consistency is required.