🔴 The Error You're Seeing

Confirm this matches your console output. If it does, you're in the right place.

ERROR LOG2026-02-18 14:05:55.800 ERROR 8842 --- [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)] with root cause org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.devinhyderabad.entity.User#1]

⚡ Quick Fix Works 80% of the time

Implement a retry mechanism using Spring Retry to catch the exception and try again.

@Retryable(value = ObjectOptimisticLockingFailureException.class, maxAttempts = 3) @Transactional public void updateUser(Long id, UserRequest req) { ... }

🧠 Why this Happens

Tap to expand the deep technical explanation

You used `@Version` for optimistic locking. User A and User B both loaded the same row. User A saved their changes first, incrementing the version. When User B tried to save, Hibernate noticed the version in the DB was higher than the version User B had, meaning the data was stale.

The HITEC City Parking Spot Analogy:

Imagine editing a Google Doc. If someone else starts editing the same paragraph, Google Docs locks it and tells you to refresh. Optimistic locking is the database equivalent of that 'refresh' warning.

🔁 How to Reproduce Confirm this is your error

Add a `@Version` field to an Entity. Fetch a record in two separate threads. Save the first thread. Wait 1 second. Save the second thread. The second save will throw this exception.

🛠️ Solutions (5 Ways to Fix)

Solution 1✓ Most common cause

Implement Retry Logic

👉 Use this if concurrent updates are rare but possible.

Use Spring Retry to automatically catch the exception and retry the transaction 2-3 times before failing.

@Retryable(value = ObjectOptimisticLockingFailureException.class, maxAttempts = 3, backoff = @Backoff(delay = 100)) @Transactional public void updateUser(Long id, UserRequest req) { User user = repo.findById(id).orElseThrow(); user.setName(req.getName()); repo.save(user); }
Solution 2

Fetch fresh data before update

👉 Use this if users are keeping forms open for too long.

Instead of passing a detached entity from the frontend, fetch the fresh entity from the DB inside the @Transactional method, update it, and save.

@Transactional public void updateUser(Long id, String newName) { User user = repo.findById(id).orElseThrow(); user.setName(newName); repo.save(user); }
Solution 3

Use Pessimistic Locking

👉 Use this if concurrent updates are highly frequent and retrying is too expensive.

Use `@Lock(LockModeType.PESSIMISTIC_WRITE)` to lock the row at the database level, forcing other transactions to wait.

@Repository public interface UserRepository extends JpaRepository<User, Long> { @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT u FROM User u WHERE u.id = :id") User findUserForUpdate(@Param("id") Long id); }
Solution 4

Fix unsaved-value mapping

👉 Use this if you are the only user and still getting this error.

If your `@Version` field is an `int` and defaults to `0`, but Hibernate expects `null` for unsaved values, it gets confused. Use `Integer` (wrapper) instead of `int` (primitive).

@Entity public class User { @Version private Integer version; // Use Integer, not int }
Solution 5

Handle the exception globally

👉 Use this to return a clean 409 Conflict HTTP status to the client.

Catch the exception and tell the user their data is stale.

@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ObjectOptimisticLockingFailureException.class) public ResponseEntity<String> handleConflict() { return ResponseEntity.status(HttpStatus.CONFLICT).body("Record was updated by another user. Please refresh."); } }

📋 Version Notes

Spring Boot 2.x

Uses Hibernate 5. Standard @Version support.

Spring Boot 3.x

Uses Hibernate 6. Better retry integration.

🛡️ How to Prevent This Next Time

Always keep transactions short and use `@Version` for entities that are frequently updated by multiple users.