All Posts

Types of Locks Explained: Optimistic vs. Pessimistic Locking

How do you prevent two users from editing the same record at the same time? We compare Optimistic Locking (Versioning) and Pessimistic Locking (Databa

Abstract AlgorithmsAbstract Algorithms
ยทยท13 min read
Cover Image for Types of Locks Explained: Optimistic vs. Pessimistic Locking
Share
AI Share on X / Twitter
AI Share on LinkedIn
Copy link

TLDR: Pessimistic locking locks the record before editing โ€” safe but slower under low contention. Optimistic locking checks for changes before saving using a version number โ€” fast but can fail and require retry under high contention. Choosing correctly depends on your write-conflict probability.


๐Ÿ“– Two Ways to Prevent the Concurrent Edit Problem

Imagine a Shared Google Doc. Two people open it simultaneously and both start typing.

  • Pessimistic approach: Only one person can type at a time. The doc is "locked" for everyone else until the first person finishes.
  • Optimistic approach: Both people can type freely. When someone saves, the system checks if the doc changed while they were typing. If it did, they get a conflict error and must retry.

Neither approach is universally better. The right choice depends on how often write conflicts actually occur in your system.

StrategyLock timingPerformanceBest when
PessimisticBefore readingSlower (blocks others)High conflict probability
OptimisticAt save time (version check)Faster (no blocking)Low conflict probability

๐Ÿ” Locking Fundamentals: Concurrency and Write Conflicts

Before picking a locking strategy, it helps to understand exactly what can go wrong without one. When two transactions read and write the same row at overlapping times, the database can produce incorrect results โ€” silently. These are called concurrency anomalies.

AnomalyWhat happensExample
Lost updateBoth transactions read the same value; each writes back an increment โ€” one write is overwrittenTwo cashiers both read stock = 10 and both sell 1; stock ends up at 9 instead of 8
Dirty readTransaction B reads a value written by Transaction A before A commits; A then rolls backB sees a balance of $500 that A wrote but later reversed
Phantom readTransaction B re-reads a range and gets extra rows that Transaction A inserted mid-queryA report counts 100 orders on first scan, 102 on the second

Locking prevents these anomalies by serialising access to shared data. Pessimistic locking prevents them by refusing to let a second transaction even see the row until the first is done. Optimistic locking prevents the lost update specifically by detecting that the row changed and refusing to commit the stale write.

Understanding which anomaly you are actually protecting against is the first step to choosing the right lock.


๐Ÿ”ข Pessimistic Locking: SELECT ... FOR UPDATE

Pessimistic locking acquires an exclusive database lock on a row before any modification.

-- Transaction A: lock the row
BEGIN TRANSACTION;
SELECT * FROM inventory WHERE product_id = 42 FOR UPDATE;

-- Now safely modify
UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 42;
COMMIT TRANSACTION;

While Transaction A holds the lock, Transaction B's SELECT ... FOR UPDATE on the same row blocks until A commits or rolls back.

sequenceDiagram
    participant A as Transaction A
    participant B as Transaction B
    participant DB as Database

    A->>DB: BEGIN + SELECT FOR UPDATE (product 42)
    DB-->>A: Row locked
    B->>DB: SELECT FOR UPDATE (product 42)
    DB-->>B: Waiting for lock...
    A->>DB: UPDATE + COMMIT
    DB-->>B: Lock acquired
    B->>DB: UPDATE + COMMIT

๐Ÿ“Š Lock State Machine

stateDiagram-v2
    [*] --> Unlocked
    Unlocked --> Locked_T1 : Thread T1 acquires
    Locked_T1 --> Contended : Thread T2 requests
    Contended --> T2_Waiting : T2 blocked
    T2_Waiting --> Locked_T2 : T1 releases lock
    Locked_T2 --> Unlocked : T2 releases
    Locked_T1 --> Unlocked : T1 releases (no contention)

Lock types:

LockBehavior
Exclusive (X)Only one holder; blocks all reads and writes
Shared (S)Multiple holders allowed for reads; blocks exclusive locks

Deadlock risk: Transaction A holds Row 1, wants Row 2. Transaction B holds Row 2, wants Row 1. Both wait forever. Databases detect this and roll back one transaction. Mitigation: always acquire locks in a consistent order; keep transactions short.


โš™๏ธ Optimistic Locking: Version Column Pattern

Optimistic locking adds a version column. No lock is held during the read โ€” only at write time does the system check whether anyone else modified the row first.

-- Step 1: Read with version
SELECT id, balance, version FROM accounts WHERE id = 1;
-- Returns: { id: 1, balance: 1000, version: 3 }

-- Step 2: Update with version check
UPDATE accounts
SET balance = 900, version = 4         -- increment version
WHERE id = 1 AND version = 3;          -- only if version unchanged

-- Step 3: Check affected rows
-- 1 row affected = success
-- 0 rows affected = conflict โ†’ retry

In Java with JPA:

@Entity
public class Account {
    @Id Long id;
    double balance;

    @Version          // JPA manages optimistic locking automatically
    int version;
}

JPA throws OptimisticLockException when the version check fails โ€” the calling service must catch it and retry.


๐Ÿง  Deep Dive: How Database Locks Are Implemented

Databases implement pessimistic locks via a lock manager: an in-memory hash table keyed by (table, row) that tracks which transaction holds each lock and which are waiting. The lock manager periodically scans wait queues for cycles to detect deadlocks, then rolls back one transaction as the victim. Optimistic locking needs no lock manager โ€” the version check happens inside the UPDATE statement itself, handled entirely by the storage engine at commit time.


๐Ÿ“Š Choosing Your Lock Strategy

The decision between pessimistic and optimistic locking is primarily a question of conflict frequency. Use the flowchart below as a quick decision guide, then validate against your observed write-conflict rate in production.

flowchart TD
    A[Write operation needed] --> B{How frequent are write conflicts?}
    B -->|High - more than 10 percent| C[Use Pessimistic Locking]
    B -->|Low - less than 5 percent| D[Use Optimistic Locking]
    C --> E{Single row or multiple rows?}
    E -->|Single row| F[SELECT FOR UPDATE]
    E -->|Multiple rows| G[Lock in consistent order to prevent deadlock]
    D --> H[Add version column to table]
    H --> I{Conflict detected on save?}
    I -->|Yes| J[Throw OptimisticLockException - retry]
    I -->|No| K[Commit successfully]

๐Ÿ“Š Lock Type Selection Decision Tree

flowchart TD
    A[Choose a lock strategy] --> B{Concurrent reads frequent?}
    B -- Yes --> C{Writes also frequent?}
    C -- No --> D[ReadWriteLock\nshared reads / exclusive writes]
    C -- Yes --> E[Pessimistic row lock]
    B -- No --> F{Single-threaded access?}
    F -- Yes --> G[No lock needed]
    F -- No --> H{Low conflict probability?}
    H -- Yes --> I[Optimistic MVCC\nversion column]
    H -- No --> J[Pessimistic lock\nSELECT FOR UPDATE]

If your write-conflict rate sits between 5 % and 10 %, measure under realistic load before deciding. Optimistic locking's retry overhead compounds quickly when conflicts are frequent, so a brief load test is worth the effort before committing to a strategy.


๐ŸŒ Real-World Applications: Choosing the Right Strategy for Real Systems

ScenarioBest choiceWhy
Inventory decrement (flash sale, limited stock)PessimisticTwo buyers cannot be allowed to buy the last item concurrently
Banking transfer between two accountsPessimisticAtomic debit + credit must be consistent
User profile update (low concurrent edit)OptimisticConflicts are rare; version check is sufficient
CMS article editing (one author at a time likely)OptimisticMost edits are by different users; blocking is unnecessary overhead
Read-heavy + occasional writeOptimisticAvoids locking on all those reads

โš–๏ธ Trade-offs & Failure Modes: Trade-offs and Failure Modes

Failure modeStrategySymptomMitigation
DeadlockPessimisticBoth transactions wait foreverConsistent lock order; short transactions; timeout + retry
StarvationPessimisticLow-priority transaction never gets the lockFairness algorithms; lock queuing
Lost updateOptimistic (if not implemented)Second writer silently overwrites firstAdd version column; throw on conflict
Retry stormOptimisticHigh contention causes endless retriesExponential backoff; switch to pessimistic for hot rows
Lock escalationPessimisticRow locks escalate to table lockAvoid locking large result sets; use indexes

The probability of lock contention under random access can be modeled as:

$$P( ext{conflict}) pprox 1 - e^{-\lambda t}$$

where $\lambda$ is the arrival rate of writers and $t$ is the average transaction duration. When $\lambda t$ is small (< 0.1), optimistic is clearly cheaper. When $\lambda t > 1$, pessimistic avoids the retry overhead.


๐Ÿงญ Decision Guide: Pessimistic vs. Optimistic

QuestionPessimistic wins when...Optimistic wins when...
How often do write conflicts occur?Frequently (> 10% of writes)Rarely (< 5% of writes)
How long are critical sections?Short (single DB transaction)Doesn't matter โ€” no blocking
Can you tolerate retry logic in the app?No โ€” retry adds complexityYes โ€” simple retry loop is acceptable
Is the record accessed by many users?Yes (hot row)No (user-private data)

๐ŸŽฏ What to Learn Next


๐Ÿงช Lock Strategy in Practice: Three Scenarios

Theory only goes so far โ€” the best way to build intuition is to walk through real-world situations and reason through the choice. Each scenario below was selected to represent a distinct point on the conflict-probability spectrum, from near-certain concurrent writes to rare cross-user collisions. As you read each row, focus on the reasoning column: notice how the same two questions โ€” "how hot is this row?" and "what is the cost of a conflict?" โ€” drive every recommendation.

ScenarioRecommended strategyReasoning
E-commerce flash sale (100 users buy the last item simultaneously)Pessimistic (SELECT FOR UPDATE)Multiple transactions must not see the same stock count concurrently. A lost update means overselling. The row is hot and conflicts are near-certain.
Banking account transfer (debit account A, credit account B atomically)Pessimistic (lock both rows in consistent ID order)Debit and credit are an atomic pair. Neither can be allowed to proceed on a stale balance. Lock order (lower ID first) prevents deadlocks.
CMS article draft (authors editing their own articles)Optimistic (version column)Each author edits their own article; simultaneous edits on the same draft are rare. Blocking would add unnecessary overhead for a very unlikely event. A conflict error is easy to surface in the UI ("Someone else saved changes โ€” please review and retry").

The pattern: if your scenario involves a shared hot resource (last item, shared balance), pessimistic wins. If each user mostly touches their own data with only occasional overlap, optimistic wins and is cheaper to operate at scale.


๐Ÿ› ๏ธ Spring @Transactional and java.util.concurrent: Locks in Production Java Code

Spring Framework's @Transactional maps directly onto the pessimistic/optimistic locking strategies in this post โ€” it translates @Lock(LockModeType.PESSIMISTIC_WRITE) into a SELECT ... FOR UPDATE and @Version into the optimistic version-check pattern, so you never write raw SQL locking statements in a Spring application. java.util.concurrent provides ReentrantLock, ReadWriteLock, and StampedLock for in-process concurrent access control that mirrors the same trade-offs at the JVM level.

import jakarta.persistence.*;
import org.springframework.data.jpa.repository.*;
import org.springframework.transaction.annotation.*;
import java.util.concurrent.locks.*;

// โ”€โ”€ JPA Optimistic Locking: @Version column โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@Entity
public class Inventory {
    @Id Long productId;
    int  quantity;

    @Version           // JPA manages version check automatically on UPDATE
    int version;       // throws OptimisticLockException when stale
}

// โ”€โ”€ JPA Pessimistic Locking: SELECT โ€ฆ FOR UPDATE via @Lock โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
public interface InventoryRepository extends JpaRepository<Inventory, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)   // โ†’ SELECT โ€ฆ FOR UPDATE
    @Query("SELECT i FROM Inventory i WHERE i.productId = :id")
    Inventory lockById(@Param("id") Long id);
}

// โ”€โ”€ Service layer using Spring @Transactional โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@Service
public class InventoryService {

    private final InventoryRepository repo;
    public InventoryService(InventoryRepository repo) { this.repo = repo; }

    // Flash sale: pessimistic lock prevents overselling
    @Transactional
    public void decrementStock(Long productId, int qty) {
        Inventory inv = repo.lockById(productId);  // holds row lock until commit
        if (inv.quantity < qty) throw new IllegalStateException("Insufficient stock");
        inv.quantity -= qty;
        repo.save(inv);
    }

    // Profile update: optimistic lock โ€” rare conflicts, no blocking
    @Transactional
    public void updateProfile(Inventory item) {
        // JPA auto-checks @Version on save; throws OptimisticLockException on conflict
        repo.save(item);
    }
}

// โ”€โ”€ In-process ReadWriteLock: multiple readers, exclusive writer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
public class CacheManager {
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Map<String, String> cache = new HashMap<>();

    public String read(String key) {
        rwLock.readLock().lock();   // many concurrent readers allowed
        try { return cache.get(key); }
        finally { rwLock.readLock().unlock(); }
    }

    public void write(String key, String value) {
        rwLock.writeLock().lock();  // exclusive โ€” all readers block during write
        try { cache.put(key, value); }
        finally { rwLock.writeLock().unlock(); }
    }
}

// โ”€โ”€ StampedLock: optimistic read (no blocking), upgradeable to write โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
public class PriceCache {
    private final StampedLock sl = new StampedLock();
    private double price = 0.0;

    public double readPrice() {
        long stamp = sl.tryOptimisticRead();   // no lock acquired
        double p = price;
        if (!sl.validate(stamp)) {             // writer intervened โ†’ upgrade to read lock
            stamp = sl.readLock();
            try { p = price; }
            finally { sl.unlockRead(stamp); }
        }
        return p;
    }

    public void setPrice(double newPrice) {
        long stamp = sl.writeLock();
        try { price = newPrice; }
        finally { sl.unlockWrite(stamp); }
    }
}

StampedLock.tryOptimisticRead() is the in-process equivalent of optimistic locking: no lock is held during the read, and validate() checks whether a write occurred โ€” matching the version-column pattern exactly. Use it when read throughput vastly outweighs write frequency.

For a full deep-dive on Spring @Transactional and java.util.concurrent, a dedicated follow-up post is planned.


๐Ÿ“š Key Takeaways on Lock Selection

Choosing the wrong lock strategy leads to either lost updates (under-locking) or throughput bottlenecks (over-locking). Keep these four rules of thumb handy:

  1. Measure conflict probability first. Don't guess. Add a counter to your retry catch block and monitor it for one week under real load. If fewer than 5 % of writes ever retry, optimistic is almost always the right call.
  2. Pessimistic locking is safe by default, not fast by default. It prevents all write anomalies, but every held lock is a potential queue. Keep pessimistic transactions as short as possible โ€” read, compute, write, commit.
  3. Optimistic locking shifts the cost to the application layer. You trade database lock contention for retry-loop complexity. Make sure your service layer has exponential backoff and a maximum retry budget.
  4. Deadlocks are a pessimistic-locking tax. They are preventable but not eliminable. Always document the lock acquisition order for multi-row transactions in your team's conventions.

๐Ÿ“Œ TLDR: Summary & Key Takeaways

  • Pessimistic locking blocks concurrent access with SELECT ... FOR UPDATE โ€” safe under high contention.
  • Optimistic locking uses a version column โ€” no blocking, but requires retry logic on conflict.
  • Deadlocks are the primary failure mode of pessimistic locking; retry storms affect optimistic locking under high contention.
  • Always acquire locks in a consistent order to prevent deadlocks.
  • Model your write-conflict probability before choosing: $P( ext{conflict}) pprox 1 - e^{-\lambda t}$.

๐Ÿ“ Practice Quiz

  1. What is the primary advantage of optimistic locking over pessimistic locking?

    • A) Optimistic locking never requires retries
    • B) Optimistic locking avoids blocking other transactions during reads, improving throughput under low contention
    • C) Optimistic locking prevents all write conflicts
    • D) Optimistic locking is always faster than pessimistic locking regardless of conflict rate

    Correct Answer: B โ€” Optimistic locking does not hold a database lock during the read phase, so other transactions can proceed freely; this is a clear throughput win when write-conflict probability is low.

  2. A deadlock occurs in a pessimistic locking system. What is the most common detection mechanism?

    • A) The database logs the error and ignores it
    • B) The database detects the wait cycle and rolls back one of the transactions
    • C) The application times out after 30 seconds
    • D) The database escalates to a table lock to resolve the conflict

    Correct Answer: B โ€” Databases use cycle-detection algorithms on the wait-for graph; when a cycle is found, one transaction is chosen as the victim and rolled back to break the deadlock.

  3. When should you prefer pessimistic locking for inventory management?

    • A) When users rarely edit the same product at the same time
    • B) When multiple buyers can simultaneously attempt to purchase the last unit and you cannot allow overselling
    • C) When read performance is the top priority
    • D) When you want to avoid adding a version column to the table

    Correct Answer: B โ€” With pessimistic locking the row is locked before the read, so no two buyers can see the same inventory count simultaneously; this is the only reliable way to guarantee no overselling.



Abstract Algorithms

Written by

Abstract Algorithms

@abstractalgorithms