Hello! Welcome back.
In our last lesson, we focused on performance by implementing efficient pagination and sorting with Spring Data's Pageable interface. You learned how to handle large datasets gracefully, preventing server-side OutOfMemoryErrors and providing a smooth user experience.
Today, we shift our focus from performance to a critical aspect of data integrity: concurrency control. In any multi-user application, it's possible for two or more users to try to modify the same piece of data at the same time. This can lead to a serious issue known as a "lost update."
Your goal for this lesson is to learn how to implement optimistic locking using the @Version annotation to prevent lost updates. This is a fundamental technique for building robust, scalable systems and a common topic in senior-level interviews, as it demonstrates a deep understanding of transaction management and data consistency.
1. The "Lost Update" Anomaly: A Race Condition in the Database
Before we can solve the problem, we must first understand it. A "lost update" occurs when two concurrent transactions read the same initial data, and one transaction's update is overwritten—or "lost"—because of the other.
Imagine an inventory system:
- Transaction A reads a product's stock: 10 items.
- Transaction B also reads the product's stock: 10 items.
- Transaction A processes a sale, calculates the new stock as 9, and writes it back to the database. The stock is now 9.
- Transaction B processes a return, calculates the new stock as 11, and writes it back to the database. The stock is now 11.
The update from Transaction A is lost. The final stock should have been 10 (10 - 1 + 1), but because Transaction B was working with stale data, the result is incorrect. This kind of data corruption can have severe business consequences.
Vlad Mihalcea - Transactions and Concurrency Control Patterns
To see a real-world, high-stakes example of this problem, watch this segment from a talk by Vlad Mihalcea, a leading expert in JPA and Hibernate. He explains how concurrency flaws led to real financial losses for cryptocurrency exchanges.
Watch from the beginning until 08:21. Pay close attention to how he demonstrates a simple money transfer function being exploited by running parallel requests, leading to an incorrect final balance. This is a classic demonstration of a race condition leading to data corruption.
The issue Vlad Mihalcea demonstrates is a direct result of the "lost update" anomaly. The application reads a balance, performs a check, and then writes a new balance, leaving a window of time where another transaction can interfere.

2. The Solution: Optimistic Locking
How do we prevent lost updates? One approach is pessimistic locking, where you lock the database row when you read it, preventing any other transaction from touching it until you're done. This is safe but can severely limit concurrency and scalability, as other users have to wait.
A more common and scalable approach for web applications is optimistic locking. The core idea is:
Assume that conflicts are rare, so don't lock the data on read.
Instead, when you attempt to write the data, verify that it hasn't been changed by another transaction since you read it.
JPA and Hibernate make this incredibly easy to implement using a version-based mechanism.
To understand the mechanics, please read the following article from Baeldung. It provides a concise explanation of how optimistic locking works in JPA.
Please read the sections '2. Understanding Optimistic Locking' and '4. Version Attributes'. Focus on: The role of the @Version annotation. How each transaction reads and holds the version value. What happens when a transaction tries to update an entity whose version has changed.
As you read, the key mechanism is the version column. Here's the flow:
- You add a field to your entity annotated with
@Version(e.g.,private int version;). Hibernate will manage this column automatically. - When a transaction reads an entity, it also reads its current
versionvalue (say,5). - When the transaction commits an update, the generated SQL will look something like this:
UPDATE product SET name = 'New Name', ..., version = 6 WHERE id = 123 AND version = 5;. - If another transaction updated the record in the meantime, the
versionin the database would already be6. TheWHEREclause of your update would match zero rows. - Hibernate detects that zero rows were affected, concludes that a concurrent modification occurred, and throws an
OptimisticLockException.
This "check-then-update" happens atomically in the database, reliably preventing lost updates.
3. Implementing Optimistic Locking in Spring Boot
Now let's see this in action. The best way to understand this is to watch someone code it and simulate the concurrent access.
Spring Boot | Optimistic & Pessimistic Locking Explained with Concurrent Movie Seat Booking Example
This video from the Java Techie channel provides a practical demonstration of setting up and testing optimistic locking in a Spring Boot application, using a movie seat booking example.
Please watch from 09:51 to 28:56. This segment covers: Entity Modification (09:51 - 11:20): Adding the @Version annotation to the Seat entity. Service Logic (11:20 - 15:23): Writing the service method to book a seat, which involves fetching and saving the entity. Concurrent Test (15:23 - 28:56): Creating two threads that try to book the same seat simultaneously. Observe the log output to see one thread succeed (incrementing the version) while the other fails with an optimistic locking exception.
The video clearly shows the outcome: one transaction succeeds, and the other is rolled back with an exception. Data integrity is preserved. The key is that the failing transaction is made aware of the conflict instead of silently overwriting another's changes.
Test your understanding!
You've added @Version to your Product entity. A user loads a product page, which fetches a Product entity with id=101 and version=3. They edit the product's description and click "Save." In the background, your @Transactional service method re-fetches the product, sets the new description, and the transaction commits.
What SQL WHERE clause will Hibernate generate for the UPDATE statement to ensure data consistency?
Show answer
Hibernate will generate an UPDATE statement with a WHERE clause that checks both the primary key and the version number that was read at the start of the transaction. The clause will be:
WHERE id = 101 AND version = 3
When this SQL executes, the database will only update the row if the version is still 3. If it has changed, the update will affect zero rows, and Hibernate will throw an OptimisticLockException. The version column itself will also be incremented in the SET clause (e.g., SET ..., version = 4).
4. Handling Optimistic Lock Exceptions
Preventing the lost update is great, but what should your application do when it catches an OptimisticLockException (or more specifically, Spring's ObjectOptimisticLockingFailureException)? You have two primary strategies.
- Automatic Retries: If the conflict is likely transient and the operation is non-interactive (e.g., a background job), you can automatically retry the entire transaction. The new transaction will read the newer version of the data and attempt the update again.
- User-Facing Feedback: If a user is editing a form, a blind retry is not a good idea. The best practice is to inform the user that the data has been modified by someone else, refresh their view with the latest data, and ask them to re-apply their changes if desired.
Resolving Optimistic Lock Exceptions in Spring Boot
This article provides excellent code examples for both handling strategies in a Spring Boot application.
Please read the section 'Handling Conflicts with Retries and Feedback'. Focus on: How to use Spring Retry's @Retryable annotation for automatic retries. How to implement a global exception handler with @RestControllerAdvice to return a meaningful HTTP 409 Conflict error to the client.
Choosing the right strategy depends on the use case, but knowing both is essential for a senior developer. For most REST APIs backing a user interface, providing clear feedback via an exception handler is the preferred approach.
Conclusion
You have now learned a powerful and scalable pattern for maintaining data consistency in the face of concurrent updates. This is a non-negotiable skill for building reliable enterprise systems.
Key Takeaways:
- Lost Update Anomaly: A critical concurrency issue where one transaction's changes are overwritten by another, leading to data corruption.
- Optimistic Locking: A concurrency control strategy that assumes conflicts are rare. It avoids database locks on read but verifies data has not changed on write.
@VersionAnnotation: The JPA mechanism to enable optimistic locking. Hibernate automatically adds and manages a version column.- The Mechanism: The
UPDATEstatement includes the version in theWHEREclause. If the version has changed, the update fails, and anOptimisticLockExceptionis thrown. - Handling Exceptions: You can either retry the transaction automatically (for background processes) or provide feedback to the user (for UI-driven actions), typically with an HTTP 409 Conflict status.
Preview of the Next Lesson:
We've seen that optimistic locking causes a transaction to be rolled back. In the next lesson, we will dive deeper into transaction rollbacks. You will learn how to diagnose transaction rollback scenarios, including the default behavior for checked vs. unchecked exceptions, a subtle but critical topic for interviews and for writing bug-free transactional code.