What Is Optimistic Locking?

What Is Optimistic Locking?

A beginner‑to‑production tour of one of the most important ideas in databases and distributed systems — explained so simply that even a 10‑year‑old could follow it, but deep enough to hold up in a real engineering interview or a real production incident.

01

Introduction & History

Optimistic locking is a way of letting many people (or many programs) change the same piece of data at the same time, without locking that data first. Instead of saying “wait your turn,” the system says: “go ahead and try — I’ll check at the very end if anyone else changed this before you finished, and if they did, you’ll have to try again.”

Simple analogy — the shared Google Doc

Imagine you and your friend both open the same Google Doc at the same time. Neither of you is told “wait, only one person can type.” You both type freely. But Google Docs quietly keeps track of the exact version of the document each of you started from. If you both try to edit the exact same word at the exact same moment, Google Docs detects the clash and helps merge or warn about it — it doesn’t lock the whole document just because two people opened it.

That is the heart of optimistic locking: assume conflicts are rare, let everyone work freely, and only deal with a conflict if one actually happens. This is the opposite of “locking the door first and making everyone wait,” which is called pessimistic locking (we will compare the two later in this tutorial).

1.1 Why Is It Called “Optimistic”?

The word “optimistic” is used because the system is optimistic — hopeful — that two people won’t collide while editing the same data. It doesn’t prepare for a fight in advance. It only reacts if a fight actually happens. This is different from being “pessimistic,” where the system assumes a collision is likely and prepares for it by blocking others upfront.

1.2 A Short History

The formal idea of optimistic concurrency control (OCC) was introduced in a well‑known 1981 research paper by H.T. Kung and John T. Robinson, titled “On Optimistic Methods for Concurrency Control.” At the time, most databases used pessimistic locking — literally locking rows or tables so that only one transaction could touch them at a time. This worked, but it caused a lot of waiting, especially as more users and more powerful computers came online in the 1980s and 1990s.

Kung and Robinson proposed a different idea: what if transactions almost never actually conflicted in practice? If that’s true, locking everything upfront wastes time. Instead, let transactions run freely, and only check for conflicts right before saving. If nothing collided, save happily. If something did collide, throw the work away and try again.

Over the following decades, this idea spread far beyond academic databases:

  • 1981The original idea. Kung & Robinson publish the concept of Optimistic Concurrency Control (OCC) as an alternative to lock‑based systems.
  • 1990sRelational databases adopt it. Databases like Oracle and early versions of SQL Server begin supporting version‑based conflict detection using row versions or timestamps.
  • 2000sORMs popularise it. Object‑Relational Mapping tools like Hibernate (Java) and Entity Framework (.NET) add built‑in @Version‑style annotations, making optimistic locking a few lines of code instead of manual SQL.
  • 2010sDistributed systems & NoSQL. Systems like Apache Cassandra (lightweight transactions), DynamoDB (conditional writes), and Git itself (which uses optimistic, version‑based merging) bring the same idea to massively distributed, non‑relational data.
  • TodayEverywhere. Optimistic locking is now a default pattern in REST APIs (ETags), microservices, mobile app sync, collaborative editors, e‑commerce inventory systems, and cloud‑native databases.
i
Key idea to remember

Optimistic locking does not literally “lock” anything while you are editing. The name is a little misleading — it should really be called “optimistic conflict detection.” Nothing is locked during editing; a check just happens at save time.

02

The Problem Optimistic Locking Solves

To understand why optimistic locking exists, we first need to understand the problem it fixes: the lost update problem.

2.1 The Lost Update Problem

Imagine a bank account with $100 in it. Two separate requests arrive at almost the same moment:

  • Request A wants to add $50 (a deposit).
  • Request B wants to subtract $30 (a withdrawal).

If both requests read the balance ($100) at the same time, and both compute their new balance based on that same original $100, here is what can go wrong:

The Lost‑Update Problem — a Silent Bug Request A (+$50) Database (bal = $100) Request B (−$30) Read balance = $100 Read balance = $100 compute 100 + 50 = 150 compute 100 − 30 = 70 Write balance = 150 Write balance = 70 (overwrites A) Final balance = $70 (WRONG) correct value: $120 — A’s deposit is lost no error was thrown, no exception fired — the money just quietly disappeared

Fig 2.1 — Both requests read the same starting value; whichever writes last silently overwrites the other. The final balance should be $120 (100 + 50 − 30), but it ends up as $70.

!
Why this is dangerous

No error was thrown. No exception happened. The system looks like it worked perfectly. But real money simply vanished. This is one of the scariest kinds of bugs — a silent, invisible data‑loss bug that only shows up when someone audits the numbers weeks later.

2.2 Where This Problem Shows Up in Real Life

E‑commerce inventory

Two customers buy the “last item in stock” at the same time. Without protection, the store might sell the same item twice.

Banking

Two transfers on the same account at once can silently overwrite each other’s balance changes, as shown above.

Ticket booking

Two people try to book the last seat on a flight or concert. Without conflict detection, both may believe they succeeded.

Collaborative documents

Two people editing the same paragraph at once. Without a conflict check, one person’s changes silently disappear.

User profile edits

A user updates their profile in two browser tabs. The older tab’s “Save” can wipe out changes made in the newer tab.

Warehouse systems

Multiple warehouse workers update stock counts on handheld scanners; stale reads cause miscounted inventory.

2.3 Why Not Just “Lock Everything” All the Time?

The obvious fix seems simple: lock the row before anyone reads it, and only release the lock after they finish writing. This is called pessimistic locking, and it does solve the lost update problem. But it introduces new problems:

Problems with always locking

  • Other users must wait, even if 99% of the time there is no real conflict.
  • If a request holding a lock crashes or hangs, everyone else can get stuck waiting forever (or until a timeout).
  • Under heavy traffic, this creates lock contention — a traffic jam where throughput collapses.
  • Locks are harder to use safely across microservices and distributed systems (they may need distributed locks, which are slow and complex).
  • Deadlocks can occur when two transactions lock resources in different orders and wait on each other forever.

What optimistic locking offers instead

  • No waiting during normal reads and edits — everyone proceeds freely.
  • Much higher throughput when conflicts are genuinely rare (the common case in most apps).
  • Works naturally across distributed and stateless systems (like REST APIs and microservices).
  • No deadlocks, because nothing is ever held and waited upon.
  • Failures are explicit and visible (a conflict exception), not silent data loss.
“Lock only when you must. Detect conflicts when you can.”

This trade‑off — waiting upfront (pessimistic) versus checking at the end (optimistic) — is one of the most fundamental design decisions in all of concurrent and distributed systems, and we will explore it in depth throughout this tutorial.

03

Core Concepts You Must Understand

Before going further, let’s build a solid vocabulary. Every term below is something you will see constantly in database documentation, interviews, and production incident reports around concurrency.

3.1 Concurrency Control

What it is: Concurrency control is the general set of techniques databases and applications use to make sure multiple operations happening “at the same time” don’t corrupt each other’s data.

Why it exists: Modern systems are never used by just one person. Multiple users, multiple app servers, and multiple background jobs all touch the same data constantly. Without concurrency control, chaos like the lost update problem happens.

Analogy: Think of a single bathroom key at a small coffee shop. Concurrency control is the set of rules for how people take turns — whether that’s a strict “one key, one line” (pessimistic) or a “just go, and we’ll sort out any overlap” (optimistic) approach.

3.2 A “Version” (or Version Number)

What it is: A version is a number (or sometimes a timestamp) stored alongside a row of data. Every time that row changes, the version number goes up by one (1, 2, 3, 4…).

Why it exists: The version number is the entire trick behind optimistic locking. It lets the system answer one simple question at save time: “Is the data still the same version I originally read, or has someone else changed it since?”

Simple example: A product row might look like {id: 42, name: "Sneakers", price: 59.99, version: 7}. If you read this row, you remember version: 7. When you try to save your update, the database checks: is the current version still 7? If yes, great — save, and bump it to 8. If no (someone else already bumped it to 8 or higher), reject your save.

The Core Optimistic Locking Loop Read Row version = 7 Make Changes in memory only Save: DB version still 7? Save Succeeds version → 8 Save Rejected conflict detected Re‑read Latest Data retry with fresh version Yes No (8+) loop

Fig 3.1 — This single loop — read, change, check version, save‑or‑retry — is the entire mechanism behind optimistic locking, no matter which technology implements it.

3.3 Timestamp‑Based Versioning

What it is: Instead of a simple counter, some systems use a “last modified” timestamp as the version marker.

Why it exists: Timestamps are convenient because they double as useful audit information (“when was this last changed?”), not just a conflict marker.

Trade‑off: Clock precision matters. If two updates happen within the same millisecond (common on fast servers), timestamps might collide even though a counter wouldn’t. This is why most production systems prefer a simple incrementing integer version column over a timestamp.

3.4 Hash‑Based / Content‑Based Versioning

What it is: Instead of a number, the system computes a hash (a fingerprint) of the entire row’s content. If any field changes, the hash changes.

Where it’s used: This is exactly how ETags work in HTTP and REST APIs, and how Git tracks whether a file has changed (via content hashes like SHA‑1/SHA‑256).

Analogy: Imagine sealing a letter with a unique wax stamp. If the letter’s content changes even slightly, the stamp pattern (hash) would be completely different. Anyone checking the stamp can instantly tell if the letter was tampered with.

3.5 Compare‑And‑Swap (CAS)

What it is: CAS is a low‑level atomic operation: “only update this value if it still equals what I expect it to be, and do this check‑and‑update as a single, uninterruptible step.”

Why it exists: CAS is the hardware and software primitive that makes optimistic locking actually safe. Without an atomic check‑and‑set, two threads could both pass the “is it still version 7?” check at the exact same nanosecond and both proceed to write — recreating the very problem we were trying to solve.

Where it’s used: CPU instructions (like x86’s CMPXCHG), Java’s AtomicInteger.compareAndSet(), SQL’s UPDATE … WHERE version = ?, and DynamoDB’s conditional writes all rely on this same idea.

i
Golden rule

The “check version” and “update version” steps must happen as one atomic database operation — typically a single SQL UPDATE statement with a WHERE version = ? clause. If you check the version in one step and update in a separate step, you have re‑introduced the exact race condition optimistic locking was meant to prevent.

3.6 Optimistic vs. Pessimistic Locking — Side by Side

AspectOptimistic LockingPessimistic Locking
When it checks for conflictAt save/commit timeBefore reading/editing (upfront)
Does it block other users?No, everyone reads/edits freelyYes, others wait for the lock to release
Best forLow‑conflict, read‑heavy workloadsHigh‑conflict, write‑heavy workloads
Risk of deadlockNonePossible
What happens on conflictSave is rejected; caller retriesCaller simply waits its turn
Works well in distributed systems?Yes, very naturally (e.g. REST, microservices)Harder — needs distributed locks
User experience on conflict“Someone else changed this, please retry”Invisible delay while waiting
Real‑life analogy — restaurant table booking

Pessimistic: You call the restaurant and they lock a table just for you the moment you start deciding — no one else can even look at that table until you confirm or cancel.

Optimistic: The restaurant lets multiple people browse the same table online. Whoever clicks “Confirm Booking” first gets it. If you click a second later, you see “Sorry, this table was just booked — please choose another.”

04

Architecture & Components

Optimistic locking is not one single tool — it’s a pattern made up of a few cooperating pieces. Let’s break down each component.

1. Version column

A field in the table (e.g. version INT) or document (e.g. a JSON field) that increments on every successful write.

2. Read path

Whenever data is fetched, the current version travels along with it, usually held in application memory or sent to the client (e.g. as an HTTP ETag header).

3. Conditional write

The update statement includes a condition: “only apply this write if the version still matches.” This is the enforcement point.

4. Conflict detector

The logic (often just “0 rows affected”) that determines whether the conditional write succeeded or failed.

5. Retry / resolution logic

Application code that decides what happens after a conflict: retry automatically, merge changes, or surface an error to the user.

6. Client / API layer

In web systems, this is where ETags, If‑Match headers, or version fields in request bodies live.

The Full Architecture of an Optimistic Locking Flow Client / Application Read Data + Version GET /products/42 Edit Locally no DB round trip Send Update payload + orig. version If‑Match: "7" Application Server Receive Request validate + auth Build Conditional Query UPDATE … WHERE id=? AND version=? Database Row Storage id | name | price | version version column drives the check Rows Affected = 1 (success) Rows Affected = 0 (conflict!) response flows back to server & client nothing is locked between the read and the write — the version check is the only synchronisation point

Fig 4.1 — The client reads data along with its version, edits locally without touching the database again, then submits both the change and the original version. The database only applies the update if the version still matches.

4.1 How This Maps to Real Technologies

LayerRelational DB (SQL)NoSQL (e.g. DynamoDB)REST API
Version storageversion or updated_at columnattribute in the item, e.g. VersionETag header / version field in JSON
Conditional writeUPDATE … WHERE version = ?ConditionExpression: "version = :v"If‑Match: "etag‑value" header on PUT/PATCH
Conflict signal0 rows updatedConditionalCheckFailedExceptionHTTP 412 Precondition Failed
ORM supportHibernate/JPA @VersionAWS SDK conditional expressionsSpring, Express, most frameworks support ETags
05

How Optimistic Locking Actually Works Internally

Let’s go step by step through exactly what happens under the hood, using a relational database as the example, since it’s the most common real‑world case.

5.1 Step‑by‑Step Walkthrough

  1. Table setup: A table has an extra integer column, commonly named version, starting at 0 or 1 for every new row.
  2. Read: The application runs SELECT id, name, price, version FROM products WHERE id = 42; and gets back version = 7.
  3. Edit in memory: The application changes the price locally. No database lock is held. Other users can freely read and edit the same row too.
  4. Conditional write: To save, the application runs:
    UPDATE products SET price = 64.99, version = version + 1 WHERE id = 42 AND version = 7;
  5. Database checks rows affected:
    • If exactly 1 row was updated → success. The version is now 8.
    • If 0 rows were updated → someone else already changed this row (version is no longer 7) → this is a conflict.
  6. Handle the result: On success, continue normally. On conflict, the application typically re‑reads the row (now at version 8, with the newest data), and either retries the change, merges it, or tells the user to review the new data.
i
Why this is safe under concurrency

Relational databases guarantee that a single UPDATE statement is atomic — it cannot be interrupted halfway by another transaction. So even if two updates race to change the same row at literally the same instant, the database will process them one after another internally, and only one of them will find version = 7 true. The other will see 0 rows affected.

5.2 Java Example — Manual Optimistic Locking with JDBC

This example shows the raw mechanics without any framework “magic,” so you can see exactly what’s happening.

Java — manual optimistic locking with JDBC
public boolean updatePrice(Connection conn, long productId, double newPrice, int expectedVersion) throws SQLException {
    String sql = "UPDATE products SET price = ?, version = version + 1 "
               + "WHERE id = ? AND version = ?";

    try (PreparedStatement stmt = conn.prepareStatement(sql)) {
        stmt.setDouble(1, newPrice);
        stmt.setLong(2, productId);
        stmt.setInt(3, expectedVersion);

        int rowsUpdated = stmt.executeUpdate();

        // rowsUpdated == 1 means success.
        // rowsUpdated == 0 means someone else already changed this row.
        return rowsUpdated == 1;
    }
}

If updatePrice() returns false, the calling code should re‑fetch the row and either retry or notify the user of the conflict.

5.3 Java Example — Optimistic Locking with JPA / Hibernate (the “easy mode”)

Most real projects don’t write raw SQL for this — they use an ORM’s built‑in @Version annotation, and the framework generates the conditional UPDATE automatically.

Java — JPA / Hibernate entity with @Version
@Entity
@Table(name = "products")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private Double price;

    @Version   // <-- This single annotation enables optimistic locking
    private Integer version;

    // getters and setters omitted for brevity
}
Java — Spring service using @Version transparently
@Service
public class ProductService {

    @Autowired
    private ProductRepository repository;

    @Transactional
    public void updatePrice(Long productId, Double newPrice) {
        Product product = repository.findById(productId)
                                     .orElseThrow(() -> new EntityNotFoundException());
        product.setPrice(newPrice);
        repository.save(product);
        // Hibernate automatically adds "AND version = ?" to the UPDATE,
        // and throws OptimisticLockException if 0 rows were affected.
    }
}

Hibernate throws jakarta.persistence.OptimisticLockException (or Spring’s ObjectOptimisticLockingFailureException) automatically when a conflict is detected — no manual row‑count checking needed.

5.4 Java Example — Retry Logic Around a Conflict

Java — simple retry loop on optimistic‑lock failure
public void updatePriceWithRetry(Long productId, Double newPrice, int maxAttempts) {
    int attempts = 0;
    while (true) {
        try {
            attempts++;
            updatePrice(productId, newPrice);   // the JPA method from above
            return; // success
        } catch (ObjectOptimisticLockingFailureException ex) {
            if (attempts >= maxAttempts) {
                throw new RuntimeException("Failed after " + attempts + " attempts", ex);
            }
            // Optional: small backoff delay before retrying
            sleepQuietly(50L * attempts);
        }
    }
}

This pattern — catch, wait a little, retry — is extremely common. Most conflicts resolve successfully within 1–2 retries because true simultaneous edits are rare.

06

Data Flow & Lifecycle of a Versioned Record

Let’s trace the full lifecycle of a single row of data as it moves through reads, edits, conflicts, and retries — from birth to many updates later.

Lifecycle State Machine of a Versioned Row Created version=N Read client A Editing local memory Attempting submit update Success version + 1 Conflict retry needed row updated · cycle repeats forever version changed re‑fetch fresh data

Fig 6.1 — Every row cycles endlessly through Created → Read → Edit → Attempt → (Success or Conflict). A conflict simply loops the client back to reading fresh data — it is a normal, expected outcome, not a system failure.

6.1 Two Clients Racing — A Concrete Walkthrough

Let’s follow Client A and Client B both trying to update the same product’s stock count.

TimeClient AClient BDatabase State
t0Reads stock=10, version=3stock=10, version=3
t1Reads stock=10, version=3stock=10, version=3
t2Decides to reduce stock to 9Decides to reduce stock to 9stock=10, version=3
t3Submits UPDATE … WHERE version=3stock=9, version=4 (success)
t4Submits UPDATE … WHERE version=30 rows affected → conflict for B
t5DoneRe‑reads stock=9, version=4, retries reduce to 8stock=8, version=5 (success)

Notice: without optimistic locking, both A and B might have both succeeded in writing “stock = 9,” silently losing one of the two decrements — and the store would think it has 1 more item in stock than it really does. With optimistic locking, B is forced to see the fresh data and correctly compute 9 − 1 = 8.

6.2 Client‑Side Lifecycle in a REST API (ETags)

The exact same lifecycle applies to web APIs using ETags — a hash or version identifier for a resource, sent in HTTP headers.

Optimistic Locking Over HTTP with ETags Browser / Client REST API Server Database GET /products/42 SELECT … WHERE id=42 row (version = 7) 200 OK · ETag: "7" user edits locally PUT /products/42 · If‑Match: "7" UPDATE … WHERE id=42 AND version=7 0 rows affected 412 Precondition Failed GET again → 200 OK · ETag: "8"

Fig 6.2 — The If‑Match header carries the expected version to the server. A mismatch results in HTTP 412 Precondition Failed, telling the client to re‑fetch fresh data before retrying.

07

Advantages, Disadvantages & Trade‑offs

Every technique has costs. Understanding when optimistic locking helps — and when it quietly hurts — is what turns book knowledge into production judgement.

Advantages

  • High throughput when conflicts are rare — no one waits for locks.
  • No deadlocks — since nothing is held while waiting.
  • Scales naturally across distributed and stateless systems (microservices, REST APIs).
  • Fails loudly and safely — a conflict is an explicit error, not silent data corruption.
  • Simple mental model once understood: read, edit, conditional write, retry.
  • Works well with long user‑editing sessions (e.g., someone editing a form for 10 minutes) because nothing is locked the whole time.

Disadvantages

  • Wasted work — a failed attempt means the user’s or system’s changes are discarded and redone.
  • Poor fit for high‑contention workloads — if many clients constantly fight over the same row, retries pile up and throughput actually drops (this is called “livelock” risk).
  • Requires extra schema/design work — you must add and maintain a version column or ETag mechanism.
  • User experience burden — someone has to design what happens when a conflict occurs (auto‑retry? show a merge screen? ask the user to redo their edit?).
  • Not suitable for long‑running, resource‑scarce operations — e.g., reserving a single physical hotel room where a guaranteed hold is more appropriate.

7.1 The Core Trade‑off

Optimistic locking trades a small chance of wasted work for a much larger gain in overall concurrency.

This only pays off when conflicts are genuinely uncommon. If two users update the exact same row constantly (imagine a single shared “global counter” hit thousands of times per second), optimistic locking causes a storm of failed retries, and a different technique — like atomic increments, sharding the counter, or pessimistic locking — is usually better.

i
Rule of thumb

Optimistic locking shines when the probability of collision per row is low — typically true for user profile edits, product catalog updates, and most CRUD applications. It struggles when collision probability is high, such as a single “likes counter” on a viral post being updated thousands of times per second by different users.

08

Performance & Scalability

Optimistic locking’s performance story depends heavily on one variable: the collision rate — how often two operations really do try to touch the same row at the same time.

Low
Collision rate → High throughput
Medium
Collision rate → Some retries, still OK
High
Collision rate → Retry storms, degraded perf

8.1 Why It Scales Well Horizontally

Because optimistic locking doesn’t require holding a lock across network calls, it works beautifully with stateless application servers. You can add more app server instances behind a load balancer, and none of them need to coordinate locks with each other — the database’s atomic conditional write is the only synchronisation point, and that’s cheap and fast.

Horizontal Scaling with Optimistic Locking Load Balancer any request → any server App Server 1 stateless App Server 2 stateless App Server 3 stateless Database · version column is the sync point

Fig 8.1 — All app servers are stateless and independent. The database’s conditional update is the single, fast synchronisation point, so adding more servers doesn’t add coordination overhead.

8.2 Backoff Strategies to Avoid Retry Storms

When conflict rates rise, naive “retry immediately” logic can make things worse — many clients retry at once, collide again, retry again, and so on. The fix is exponential backoff with jitter: wait a randomised, growing delay between retries.

Java — exponential backoff with jitter around an optimistic write
public void updateWithBackoff(Runnable updateAction, int maxAttempts) {
    int attempt = 0;
    while (true) {
        try {
            attempt++;
            updateAction.run();
            return;
        } catch (ObjectOptimisticLockingFailureException ex) {
            if (attempt >= maxAttempts) throw ex;
            long baseDelay = (long) Math.pow(2, attempt) * 20; // exponential
            long jitter = ThreadLocalRandom.current().nextLong(0, 30);
            sleepQuietly(baseDelay + jitter);
        }
    }
}

Exponential backoff with jitter spreads out retries over time, dramatically reducing the odds of repeated collisions among competing clients.

8.3 Batching & Reducing Contention

Shard hot rows

Split a single heavily‑contended counter into several partial counters (e.g., 10 shards), and sum them when reading. Drastically reduces collisions.

Use atomic DB operations

For simple increments, use native atomic operations (e.g., SQL UPDATE … SET count = count + 1 without needing version checks at all) instead of read‑modify‑write.

Queue writes

Route high‑contention writes through a message queue so they’re processed one at a time in order, avoiding the need for locking altogether.

Cache reads

Serve reads from a cache (see Section 13) so the database isn’t overwhelmed just to fetch the current version before every edit attempt.

09

High Availability & Reliability

Optimistic locking interacts with availability and reliability in a few important ways, especially in distributed and replicated database setups.

9.1 Replication & Version Consistency

In systems with a primary database and one or more replicas (read replicas), a subtle danger exists: if a client reads the version from a replica that has slightly stale data (due to replication lag), it might attempt to write against an outdated version — causing an unnecessary conflict, or worse, missing a very recent update.

!
Best practice

Reads that will lead to a write (a “read‑modify‑write” cycle) should generally go to the primary database, not a lagging replica, to avoid working from stale version data. Pure read‑only views can safely use replicas.

9.2 Failure Recovery

One major reliability benefit: because optimistic locking never holds a lock, a crashed client can never leave the system stuck. Compare this to pessimistic locking, where a crashed client holding a row lock can block every other user until a timeout expires or an administrator intervenes.

Optimistic locking on crash

  • Crashed client simply never submits its update.
  • No lock is left behind — other clients are completely unaffected.
  • System self‑heals automatically with zero manual intervention.

Pessimistic locking on crash

  • Crashed client may leave a row/table locked.
  • Other clients wait until a lock timeout fires (which may be minutes).
  • May require manual DBA intervention in severe cases.

9.3 Idempotency Matters for Retries

When automatically retrying after a conflict, make sure the retried operation is idempotent (safe to repeat) or is based on fresh, re‑read data — not on stale assumptions from the first attempt. Blindly re‑applying “subtract $30” without re‑checking the new balance can reintroduce bugs.

10

Security Considerations

Optimistic locking is mostly a data‑integrity mechanism, but it does touch a few security‑relevant areas.

Don’t trust client‑supplied versions blindly

If a client (like a mobile app) sends back a version number, always validate it server‑side against the real database — never let the client dictate whether an update “should” succeed.

Prevent information leakage in conflict errors

Conflict error messages should avoid leaking sensitive details about other users’ concurrent changes (e.g., don’t reveal another user’s private edit content in the error).

Guard against version rollback attacks

Ensure a malicious client cannot submit an old, previously‑seen version number to try to “undo” newer changes. The atomic conditional update inherently prevents this if implemented correctly.

Rate‑limit retry loops

Malicious or buggy clients that retry aggressively on conflict can be used as a denial‑of‑service vector. Apply rate limiting and maximum retry caps.

10.1 Authorisation Still Applies

Optimistic locking checks whether data changed — it does not check who is allowed to change it. Authorisation (permissions, ownership checks) must still be enforced independently, before or alongside the version check.

i
Layered defence

Think of optimistic locking as one layer in a stack: authentication (who are you?), authorisation (are you allowed to edit this?), validation (is this data valid?), and optimistic locking (is this data still current?) — all four should run together on every write.

11

Monitoring, Logging & Metrics

Because conflicts are an expected part of optimistic locking (not necessarily a bug), good observability is essential to know if your system is healthy or drowning in retries.

11.1 Key Metrics to Track

MetricWhy It Matters
Conflict rate (conflicts ÷ total write attempts)A rising trend signals growing contention on specific rows or tables.
Retry count distributionMost successful operations should resolve within 1–2 retries. A long tail suggests a hot‑row problem.
Time‑to‑success (including retries)End‑to‑end latency users actually experience, including retry delays.
“Given up after max retries” countRepresents real failed operations surfaced to end users — should be as close to zero as possible.
Hot‑row identificationWhich specific record IDs generate the most conflicts — often reveals a design flaw (e.g., a single shared counter).

11.2 Logging Best Practices

  • Log every conflict with the entity ID, expected version, and actual version found — this makes debugging contention issues much easier.
  • Avoid logging full row contents if they contain sensitive data (see the Security section).
  • Correlate conflict logs with distributed tracing IDs so you can see the full request chain that led to a conflict.
i
Alerting tip

Set an alert when the conflict rate for any single entity type crosses a threshold (e.g., more than 5% of writes failing on first attempt). This is often the earliest signal of a scaling problem before users even notice slowness.

12

Deployment & Cloud Considerations

Optimistic locking behaves slightly differently depending on where and how your database runs.

Managed SQL (RDS, Cloud SQL, Azure SQL)

Works exactly like self‑hosted SQL databases — add a version column, use conditional updates. No special cloud configuration required.

DynamoDB

Built‑in support via ConditionExpression on PutItem/UpdateItem, using an item attribute as the version, with automatic optimistic locking helpers in the AWS SDK’s higher‑level libraries.

Cosmos DB

Every document automatically has an _etag field; passing an If‑Match header on writes gives you optimistic concurrency out of the box.

Multi‑region deployments

In multi‑region active‑active setups, version checks must account for cross‑region replication lag — conflicts may need more sophisticated resolution (e.g., “last writer wins” plus conflict‑free merge strategies) since strict global ordering is expensive.

12.1 CAP Theorem Connection

The CAP theorem says a distributed system can only fully guarantee two of three properties during a network partition: Consistency, Availability, and Partition tolerance. Optimistic locking, by relying on an atomic conditional check at write time, generally favours consistency — it will reject a write rather than risk an inconsistent state, even if that temporarily reduces availability for that specific write (the client must retry). This is different from purely available “last write wins” systems that never reject writes but risk losing data silently — exactly the lost‑update problem we started with.

i
In distributed consensus terms

Optimistic locking is a lightweight cousin of full distributed consensus algorithms (like Raft or Paxos). Where Raft coordinates agreement across many nodes for every write, optimistic locking achieves a similar “only one writer wins” guarantee within a single, strongly‑consistent data store — much simpler, but only safe within that store’s consistency boundary.

13

Databases, Caching & Load Balancing

Optimistic locking isn’t just a database feature — it interacts with almost every other layer of a modern stack. Here’s how it plays out in the parts of your architecture where data is stored, cached, and routed.

13.1 Relational Databases

Almost every relational database (PostgreSQL, MySQL, SQL Server, Oracle) supports optimistic locking through the exact same pattern: a version column plus a conditional UPDATE. No special database feature is required — it’s built entirely from ordinary SQL and the guarantee that a single statement executes atomically.

13.2 NoSQL Databases

DatabaseMechanism
DynamoDBConditional expressions on writes (e.g., attribute_not_exists or version equality checks)
MongoDBManual version field pattern, checked in the findOneAndUpdate filter
CassandraLightweight Transactions (LWT) using IF clauses, e.g. UPDATE … IF version = 7
Couchbase / CouchDBBuilt‑in document revision IDs (_rev) checked automatically on every write

13.3 Caching Layer Interactions

When a cache (like Redis or Memcached) sits in front of the database, optimistic locking introduces a subtlety: the cached copy of a row can become stale the instant the underlying database row changes. Two common strategies handle this:

Cache invalidation on write

Whenever a version‑checked write succeeds, immediately invalidate (delete) the cached entry so the next read fetches fresh data with the new version.

Store the version in the cache

Include the version number as part of the cached value, so consumers reading from cache still know exactly which version they hold before attempting an edit.

This pairs naturally with the cache‑aside pattern, where the application reads from cache first, falls back to the database on a miss, and writes through to the database (invalidating the cache) on updates.

13.4 Load Balancers

Load balancers don’t need any special awareness of optimistic locking — since the technique works per‑request and doesn’t require session affinity (“sticky sessions”) or shared in‑memory locks between servers, any request can be routed to any healthy application server, and the database remains the single source of truth for version checks.

i
Why this matters for microservices

This “no shared server state required” property is exactly why optimistic locking is the default choice for concurrency control in cloud‑native, horizontally‑scaled, load‑balanced architectures — it needs nothing more than the database itself to stay correct.

14

APIs & Microservices

Optimistic locking maps beautifully onto the world of HTTP APIs and independently deployed services — in fact, HTTP itself was designed with it in mind.

14.1 REST APIs and ETags

As shown earlier, HTTP has first‑class support for optimistic concurrency via ETags and conditional headers:

  • ETag — a response header identifying the current version/fingerprint of a resource.
  • If‑Match — a request header on PUT/PATCH/DELETE that says “only perform this operation if the ETag still matches.”
  • 412 Precondition Failed — the standard HTTP status code returned when the If‑Match check fails.

14.2 GraphQL and gRPC

GraphQL and gRPC don’t have built‑in ETag support like REST, so teams typically add an explicit version field to their schema/proto messages and implement the same check‑and‑update pattern manually in resolver or service code.

14.3 Microservices — Avoiding Distributed Transactions

In a microservices architecture, data often lives in separate databases owned by separate services. Optimistic locking helps avoid the need for slow, complex distributed transactions (like two‑phase commit) within a single service’s own database, while patterns like the Saga pattern handle consistency across multiple services, often using optimistic checks at each individual step.

Optimistic Locking Across Microservice Boundaries Client Order Service Inventory Service POST /orders (place order) PATCH /stock/42 · If‑Match: "12" 200 OK · new ETag "13" 201 Created (order confirmed) alternative outcome: if another order already changed the ETag, Inventory Service returns 412 Precondition Failed — Order Service can retry with fresh data or reject the order gracefully

Fig 14.1 — Each service call carries the expected version, letting downstream services safely reject stale requests without needing a shared distributed lock across services.

14.4 Java Example — Conditional Update via a REST Client

Java — PATCH with If‑Match using Spring’s RestTemplate
public boolean updateStockViaApi(RestTemplate restTemplate, String productId, int newStock, String etag) {
    HttpHeaders headers = new HttpHeaders();
    headers.set("If-Match", etag);
    HttpEntity<StockUpdateRequest> request =
            new HttpEntity<>(new StockUpdateRequest(newStock), headers);

    try {
        restTemplate.exchange(
            "/inventory/products/" + productId + "/stock",
            HttpMethod.PATCH, request, Void.class);
        return true; // success
    } catch (HttpClientErrorException.PreconditionFailed ex) {
        return false; // conflict: version changed, caller should retry
    }
}
15

Design Patterns & Anti‑patterns

A small number of named patterns keep showing up around optimistic locking because they answer recurring problems well. An equally small number of anti‑patterns keep showing up because they’re the shortcuts everyone is tempted to take.

15.1 Recommended Patterns

Read‑modify‑write with retry

The standard pattern covered throughout this tutorial: read + version, edit, conditional write, retry on failure.

Optimistic‑then‑pessimistic fallback

Try optimistic locking first; if a specific row shows persistently high conflict rates, fall back to a short‑lived pessimistic lock just for that hot row.

Merge‑on‑conflict

Instead of rejecting outright, intelligently merge non‑overlapping field changes (e.g., two edits to different fields of the same row can both be kept) — common in collaborative document editors.

Idempotent command pattern

Model writes as commands (e.g., “increment stock by 1”) rather than raw value overwrites, reducing the actual need for strict version checks in some cases.

15.2 Anti‑patterns to Avoid

!
Anti‑pattern — check‑then‑act (non‑atomic)

Reading the version, checking it in application code, and then issuing a separate UPDATE without the version in the WHERE clause. This reopens the exact race condition optimistic locking is meant to close, because another write can sneak in between the check and the act.

!
Anti‑pattern — infinite silent retries

Retrying forever without a maximum attempt count or backoff can hang a request indefinitely and hide a deeper contention problem from monitoring.

!
Anti‑pattern — optimistic locking for scarce physical resources

For things like “only one delivery truck available,” where a failed attempt is costly and conflicts are frequent, pessimistic locking or a reservation queue is usually a better fit than repeated optimistic retries.

!
Anti‑pattern — forgetting to return the new version

After a successful update, the client must receive the new version/ETag. If it keeps using the old one, its very next edit will always fail as a false conflict.

16

Best Practices & Common Mistakes

The gap between an optimistic locking implementation that scales gracefully and one that quietly collapses under load is rarely a technology gap — it’s a discipline gap. These are the habits successful teams share.

16.1 Best Practices

Do this

  • Always make the version check and the update one atomic statement.
  • Return the new version/ETag to the client after every successful write.
  • Set a sensible maximum retry count with exponential backoff.
  • Log and monitor conflict rates per entity type.
  • Choose a simple incrementing integer version column over timestamps when possible.
  • Combine with authorisation checks — a version match doesn’t mean the user is allowed to write.
  • Design clear UX for conflicts (e.g., “This item was updated by someone else — refresh to see the latest”).

Avoid this

  • Splitting the version check and update into two separate database calls.
  • Using optimistic locking for extremely hot, high‑contention single rows without sharding.
  • Silently swallowing conflict exceptions without surfacing them anywhere.
  • Assuming client‑supplied version numbers are trustworthy without server‑side verification.
  • Forgetting that replicas can serve stale versions in a read‑modify‑write flow.

16.2 Common Mistakes (Interview & Real‑World Favourites)

MistakeConsequenceFix
Using SELECT then separate UPDATE without version in WHERELost update bug reappearsAlways include WHERE version = ? in the update itself
Not handling the zero‑rows‑affected caseApplication silently believes the write succeeded when it didn’tAlways check rows‑affected count and raise a conflict error on 0
Retrying without re‑reading fresh dataRetry fails again and again on the same stale versionAlways re‑fetch before retrying
Applying optimistic locking to a hot shared counterHigh failure/retry rate, degraded throughputUse atomic increments or sharded counters instead
17

Real‑World & Industry Examples

Almost every recognisable digital experience uses optimistic locking somewhere in its stack. Here’s a quick tour of how large systems apply it in production.

Netflix

Netflix’s microservices use optimistic concurrency in several data stores (including Cassandra’s lightweight transactions) to safely update shared state like viewing progress and account settings across many concurrent devices per user.

Amazon / E‑commerce

Inventory and cart systems commonly use conditional writes (similar to DynamoDB’s ConditionExpression) to prevent overselling the last unit of a product during flash sales.

GitHub / Git

Git itself is fundamentally optimistic: developers branch and commit freely offline, and conflicts are only detected at merge/push time, using content hashes as the “version” mechanism.

Google Docs / Office 365

Real‑time collaborative editors use optimistic, version‑aware operational transforms or CRDTs so multiple people can type simultaneously, resolving conflicts intelligently instead of locking the whole document.

Uber

Ride‑matching and driver‑status systems use optimistic version checks to avoid double‑assigning the same driver to two different ride requests that arrive at nearly the same time.

Ticketing platforms

Seat and ticket inventory systems use conditional updates so two customers racing for the last seat get a clean “sold out, please choose another seat” instead of a double‑booking.

17.1 Everyday Software Example — a Blog’s Edit‑Post API

Imagine a blogging platform’s “Edit Post” API. Author A opens a post to edit at 10:00 AM (version 5) and gets distracted, leaving the tab open for an hour. Meanwhile, Author B (a co‑editor) fixes a typo at 10:15 AM, saving successfully (version becomes 6). When Author A finally clicks “Save” at 11:00 AM, the API rejects the write with 412 Precondition Failed because their local version (5) no longer matches. The UI then shows: “This post was updated by someone else. Please review the latest version before saving your changes.” This prevents Author A from silently erasing Author B’s typo fix.

18

Advanced Topics: MVCC, Isolation Levels & Consensus

To really master optimistic locking, it helps to see how it relates to a few deeper concepts in database and distributed‑systems theory. This chapter is the interview‑ready deep dive that turns “I’ve used @Version” into “I understand what’s happening under the hood.”

18.1 Multi‑Version Concurrency Control (MVCC)

What it is: MVCC is a technique used internally by databases like PostgreSQL, MySQL (InnoDB), and Oracle, where instead of overwriting a row in place, the database keeps multiple “snapshot” versions of a row around. Each transaction sees a consistent snapshot as of when it started, even while other transactions are writing new versions concurrently.

Why it exists: MVCC lets readers never block writers and writers never block readers, which massively improves concurrency for read‑heavy workloads.

How it relates to optimistic locking: MVCC is what makes many databases naturally optimistic under the hood — every transaction is effectively “trying its luck” against a snapshot and gets validated at commit time. Application‑level optimistic locking (the version column pattern in this tutorial) is a deliberate, visible extension of this same idea, applied to a specific business entity rather than the whole transaction.

18.2 Isolation Levels and Where Optimistic Locking Fits

Isolation LevelWhat It PreventsRelation to Optimistic Locking
Read UncommittedNothing much — dirty reads possibleOptimistic locking still protects the final write via the version check
Read CommittedDirty readsMost common default; optimistic locking adds protection against lost updates that this level alone does not guarantee
Repeatable ReadDirty reads, non‑repeatable readsReduces (but doesn’t always eliminate) the need for manual version checks, depending on the database
SerializableAll anomalies, including lost updatesStrongest guarantee, but often implemented internally using an optimistic validation phase (e.g., PostgreSQL’s Serializable Snapshot Isolation)

This table matters for interviews and real design decisions: many engineers assume a strict isolation level alone prevents the lost update problem, but at the common “Read Committed” level (the default in most production databases), it does not — which is exactly why explicit optimistic locking (or Serializable isolation, which is often slower) remains necessary in application code.

18.3 Compare‑And‑Swap at the Hardware Level

The same “optimistic” philosophy exists at the CPU level. Modern processors provide an atomic Compare‑And‑Swap (CAS) instruction (e.g., CMPXCHG on x86), which multi‑threaded programs use to update shared memory without traditional locks. Java’s java.util.concurrent.atomic package (like AtomicInteger, AtomicReference) is built directly on this hardware primitive.

Java — AtomicInteger is optimistic locking in miniature
AtomicInteger stock = new AtomicInteger(10);

boolean sellOneUnit() {
    int current;
    int updated;
    do {
        current = stock.get();
        if (current <= 0) return false; // sold out
        updated = current - 1;
    } while (!stock.compareAndSet(current, updated)); // retry loop, just like DB retries
    return true;
}

This is optimistic locking in miniature, entirely in memory: read the current value, compute the new value, and atomically swap only if nothing changed in between — retrying automatically if another thread beat you to it.

18.4 Consensus Algorithms — The Distributed Cousin

In a single database, one atomic UPDATE … WHERE version = ? is enough to guarantee correctness. But in a fully distributed system with multiple independent nodes (no single database to rely on), achieving the same “only one writer wins” guarantee requires a consensus algorithm like Raft or Paxos. These algorithms let a cluster of nodes agree on a single, ordered sequence of changes, even if some nodes fail or messages are delayed. You can think of optimistic locking as solving this problem cheaply when you have one authoritative data store, while consensus algorithms solve the harder, more general version of the same problem when authority itself is distributed across many nodes.

18.5 Partitioning (Sharding) and Version Columns

When a table is partitioned (sharded) across multiple physical database nodes, the version column still works correctly as long as all reads and conditional writes for a given row are routed to the same shard — which is naturally guaranteed if you shard by the row’s primary key. Problems only arise if a system tries to coordinate a version check across multiple shards at once, which typically requires a distributed transaction coordinator or a Saga‑style pattern instead of a single conditional update.

i
Interview‑relevant insight

If asked to compare optimistic locking with MVCC, the cleanest answer is: MVCC is a database’s internal mechanism for giving transactions consistent snapshots without blocking readers; optimistic locking (the version‑column pattern) is an application‑level technique, often layered on top of MVCC, to prevent lost updates on a specific business entity when a transaction commits.

19

Frequently Asked Questions

A short set of the questions engineers, students, and interviewers ask most about optimistic locking.

Is optimistic locking actually a “lock” at all?

Not in the traditional sense. Nothing is held or blocked while editing happens. It’s really a conflict‑detection mechanism applied at save time. The name is historical, inherited from the 1981 paper that first described it as an alternative to lock‑based concurrency control.

What happens to the user’s unsaved changes when a conflict occurs?

This depends entirely on the application’s design. Common approaches include: showing an error and asking the user to redo their edit on fresh data, automatically retrying with fresh data, or presenting a merge screen showing both versions side by side.

Can optimistic locking completely eliminate all race conditions?

It eliminates the lost update problem for the specific row/resource being version‑checked, as long as the check‑and‑update is truly atomic. It does not automatically protect multi‑row or multi‑resource transactions unless every involved row/resource is version‑checked consistently.

Should I always use optimistic locking instead of pessimistic locking?

No. Choose based on your conflict rate. Low‑conflict, high‑read workloads favour optimistic locking. High‑conflict workloads on the same scarce resource (like a single seat or single truck) often favour pessimistic locking or a dedicated reservation/queue system.

How is optimistic locking different from optimistic concurrency control (OCC) in general?

They’re essentially the same idea. “Optimistic locking” is typically the term used in application/database contexts (version columns, ORMs), while “OCC” is the broader academic/theoretical term covering the same principle across databases, distributed systems, and even hardware‑level compare‑and‑swap operations.

Does optimistic locking work well for financial transactions?

Yes, it’s commonly used for individual account balance updates (as in our bank example), but complex multi‑account transfers often combine optimistic locking on each individual account with a higher‑level transaction boundary to keep the whole operation consistent.

20

Summary & Key Takeaways

Optimistic locking is a concurrency control strategy built on a simple bet: most of the time, two people won’t try to change the same piece of data at the exact same moment — so why make everyone wait? Instead, every piece of data carries a version marker. Anyone can read and edit freely. Only at the moment of saving does the system perform one atomic check: “is this still the version I started from?” If yes, the save succeeds and the version increments. If no, the save is rejected, and the application (or user) tries again with fresh data.

This pattern shows up everywhere — from a single SQL UPDATE … WHERE version = ? statement, to Hibernate’s @Version annotation, to HTTP’s ETag and If‑Match headers, to Git’s content‑hash‑based merges, to DynamoDB’s conditional writes. It scales beautifully across stateless, horizontally‑scaled, cloud‑native architectures because it needs no shared locks between servers — just one atomic guarantee from the underlying data store.

Its cost is wasted work on conflict and the design burden of deciding what to do when a conflict happens. Its reward is high throughput, no deadlocks, and safe, self‑healing behaviour even when clients crash. Used in the right place — low‑to‑medium contention data — it is one of the simplest, most powerful tools in a software engineer’s concurrency toolkit.

Key Takeaways

  • 01Optimistic locking detects conflicts at save time instead of preventing them upfront with locks.
  • 02It works using a version number, timestamp, or hash attached to each row/resource.
  • 03The version check and the update must be one atomic operation to be safe.
  • 04It solves the classic lost update problem, where the last writer silently erases someone else’s changes.
  • 05Best suited for low‑to‑medium conflict workloads; struggles under very high contention on the same resource.
  • 06Widely implemented via ORM @Version fields, SQL conditional updates, and HTTP ETags / If‑Match.
  • 07Pairs well with retry logic, exponential backoff, and good monitoring of conflict rates in production.
  • 08Compare against pessimistic locking when contention is genuinely high or the cost of a failed attempt is very expensive.
“Assume the best, but verify at the last possible moment — that is the entire philosophy of optimistic locking in one line.”