Transactions & Concurrency
Optimistic vs Pessimistic Locking — Preventing Data Corruption
Using @Version annotations vs SELECT FOR UPDATE database locks.
Introduction
When multiple users attempt to read and update the same database record concurrently, data conflicts can arise. To prevent the **Lost Update anomaly** (where User A's changes are silently overwritten by User B), applications must enforce concurrency control using **Optimistic Locking** or **Pessimistic Locking**.
Why It Matters
Without concurrency locks, systems suffer from critical data bugs. For example, two customers checking out a seat in a ticket booking system at the same time could both be allocated the same seat, or account balances could be decremented incorrectly, corrupting ledger integrity.
Real-World Analogy
Think of locks like reserving seats in a restaurant:
• Optimistic Locking (Google Docs style): Two parties read the menu. They write down their order. When submitting, the kitchen checks if the table has ordered already. If another party submitted an order for that table first, the system tells you to refresh and order again.
• Pessimistic Locking (Library book style): You claim the table physical menu card. No one else can look at or order for that table until you give the menu card back to the waiter.
Detailed Mechanics
1. Optimistic Locking
Optimistic locking assumes conflicts are rare. It uses a @Version attribute (a number or timestamp) on the entity:
If another transaction committed an update while this transaction was running, the database row version would already be 2. The update query affects 0 rows. Hibernate detects this and throws an OptimisticLockException (wrapped as ObjectOptimisticLockingFailureException by Spring), prompting a retry.
2. Pessimistic Locking
Pessimistic locking assumes conflicts are highly likely. It locks the database record at the database engine level during the initial query. Spring Data JPA generates a SELECT ... FOR UPDATE query. Any other transaction attempting to read or modify this row is blocked and queued until the current transaction commits or rolls back.
3. Feature Comparison
| Lock Strategy | Lock Location | Performance Trade-off | Best Fit |
|---|---|---|---|
| Optimistic (@Version) | Application Level | High throughput (No DB lock contention) | Read-heavy, low conflict risk |
| Pessimistic (FOR UPDATE) | Database Level | Lower throughput (Holds DB locks, blocks threads) | Write-heavy, high conflict risk |
Practical Example
Quick Quiz
Q1: What exception is thrown if a concurrent update fails version check in an optimistic lock setup?
A) DeadlockLoserDataAccessException
B) ObjectOptimisticLockingFailureException (wrapping OptimisticLockException)
C) RollbackException
D) StaleConnectionException
Answer: B — Hibernate raises an OptimisticLockException which Spring translates into a standard locking failure exception.
Q2: When does a pessimistic write lock become released?
A) As soon as the select query completes execution.
B) Only when the Java application shuts down.
C) When the containing transaction commits or rolls back.
D) After a default timeout of 10 seconds.
Answer: C — Relational databases hold row-level locks for the complete duration of the active transaction session.
Scenario-Based Challenge
Production Scenario:
During a high-concurrency ticket sale event, users booking seats complain about receiving error pages stating "Optimistic locking conflict occurred". The UX is terrible because users have to click "Book" multiple times. What is the recommended fix?
View SolutionThis happens because high contention causes version collisions. For hot-spots like seat bookings or stock items inventory:
- Switch to Pessimistic Locking: Query the booking status using
LockModeType.PESSIMISTIC_WRITE. This forces threads to queue up and execute sequentially, preventing duplicate bookings and version retry conflicts. - Implement Application Retry: Add an interceptor or use Spring's
@Retryableto retry the booking operation automatically 3 times before returning an error to the user.
Interview Questions
1. Conceptual: What is a deadlock and how does it happen with pessimistic locking?
A deadlock happens when Transaction A locks Row 1 and waits for Row 2, while Transaction B locks Row 2 and waits for Row 1. Both transactions wait indefinitely. Database engines resolve this by forcibly terminating one of the transactions, throwing a deadlock exception.
2. Concept: How can you specify a lock timeout for pessimistic lock queries?
You can specify query hints inside the repository method:
Production Considerations
Use Optimistic Locking by default (adding a @Version column to all major entities). Switch to Pessimistic Locking only for high-concurrency write hot-spots where contention is high. Ensure that all locked columns are backed by database indexes to prevent lock escalation from row-level locks to full table scans.