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.
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.”
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.
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.
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:
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.
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.
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.
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.
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
| Aspect | Optimistic Locking | Pessimistic Locking |
|---|---|---|
| When it checks for conflict | At save/commit time | Before reading/editing (upfront) |
| Does it block other users? | No, everyone reads/edits freely | Yes, others wait for the lock to release |
| Best for | Low‑conflict, read‑heavy workloads | High‑conflict, write‑heavy workloads |
| Risk of deadlock | None | Possible |
| What happens on conflict | Save is rejected; caller retries | Caller 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 |
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.”
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.
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
| Layer | Relational DB (SQL) | NoSQL (e.g. DynamoDB) | REST API |
|---|---|---|---|
| Version storage | version or updated_at column | attribute in the item, e.g. Version | ETag header / version field in JSON |
| Conditional write | UPDATE … WHERE version = ? | ConditionExpression: "version = :v" | If‑Match: "etag‑value" header on PUT/PATCH |
| Conflict signal | 0 rows updated | ConditionalCheckFailedException | HTTP 412 Precondition Failed |
| ORM support | Hibernate/JPA @Version | AWS SDK conditional expressions | Spring, Express, most frameworks support ETags |
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
- Table setup: A table has an extra integer column, commonly named version, starting at 0 or 1 for every new row.
- Read: The application runs SELECT id, name, price, version FROM products WHERE id = 42; and gets back version = 7.
- 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.
- Conditional write: To save, the application runs:
UPDATE products SET price = 64.99, version = version + 1 WHERE id = 42 AND version = 7; - 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.
- 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.
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.
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.
@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
}@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
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.
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.
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.
| Time | Client A | Client B | Database State |
|---|---|---|---|
| t0 | Reads stock=10, version=3 | — | stock=10, version=3 |
| t1 | — | Reads stock=10, version=3 | stock=10, version=3 |
| t2 | Decides to reduce stock to 9 | Decides to reduce stock to 9 | stock=10, version=3 |
| t3 | Submits UPDATE … WHERE version=3 | — | stock=9, version=4 (success) |
| t4 | — | Submits UPDATE … WHERE version=3 | 0 rows affected → conflict for B |
| t5 | Done | Re‑reads stock=9, version=4, retries reduce to 8 | stock=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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
| Metric | Why It Matters |
|---|---|
| Conflict rate (conflicts ÷ total write attempts) | A rising trend signals growing contention on specific rows or tables. |
| Retry count distribution | Most 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” count | Represents real failed operations surfaced to end users — should be as close to zero as possible. |
| Hot‑row identification | Which 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.
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.
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.
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.
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
| Database | Mechanism |
|---|---|
| DynamoDB | Conditional expressions on writes (e.g., attribute_not_exists or version equality checks) |
| MongoDB | Manual version field pattern, checked in the findOneAndUpdate filter |
| Cassandra | Lightweight Transactions (LWT) using IF clauses, e.g. UPDATE … IF version = 7 |
| Couchbase / CouchDB | Built‑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.
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.
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.
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
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
}
}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
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.
Retrying forever without a maximum attempt count or backoff can hang a request indefinitely and hide a deeper contention problem from monitoring.
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.
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.
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)
| Mistake | Consequence | Fix |
|---|---|---|
| Using SELECT then separate UPDATE without version in WHERE | Lost update bug reappears | Always include WHERE version = ? in the update itself |
| Not handling the zero‑rows‑affected case | Application silently believes the write succeeded when it didn’t | Always check rows‑affected count and raise a conflict error on 0 |
| Retrying without re‑reading fresh data | Retry fails again and again on the same stale version | Always re‑fetch before retrying |
| Applying optimistic locking to a hot shared counter | High failure/retry rate, degraded throughput | Use atomic increments or sharded counters instead |
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.
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 Level | What It Prevents | Relation to Optimistic Locking |
|---|---|---|
| Read Uncommitted | Nothing much — dirty reads possible | Optimistic locking still protects the final write via the version check |
| Read Committed | Dirty reads | Most common default; optimistic locking adds protection against lost updates that this level alone does not guarantee |
| Repeatable Read | Dirty reads, non‑repeatable reads | Reduces (but doesn’t always eliminate) the need for manual version checks, depending on the database |
| Serializable | All anomalies, including lost updates | Strongest 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.
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.
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.
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.
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.”