A few years ago, I investigated a production issue where customers occasionally reported incorrect inventory counts. The application was healthy. The database was healthy. No errors appeared in the logs.
The problem turned out to be concurrent updates. Multiple requests were modifying the same inventory record at nearly the same time, and one update silently overwrote another. The database did exactly what it was asked to do. The application failed to co-ordinate concurrent modifications to shared data.
This is a common consistency problem. Whenever multiple users, services, or processes attempt to modify the same data simultaneously, contention appears.
To manage that contention, systems typically rely on two approaches Optimistic locking and Pessimistic locking. Both aim to preserve data consistency, but they make very different assumptions about how conflicts occur. Those assumptions directly affect performance, scalability, and user experience.
1. Why Locking Exists
Databases are excellent at storing and retrieving data, but they do not inherently understand business intent. They execute operations exactly as instructed. This becomes problematic when multiple users interact with the same piece of data at the same time.
Consider an inventory record:
Product A
Inventory = 10
Now imagine two users accessing the system simultaneously. Both users read the same inventory value:
Inventory = 10
User A purchases one item.
User B purchases two items.
The timeline looks like this:
User A reads 10
User B reads 10
User A writes 9
User B writes 8
Both transactions succeed from the database's perspective. No errors occur, and both updates are accepted. However, one update effectively overwrites the other.
This scenario is known as a lost update. Both users started with the same information, but because their updates were not co-ordinated, one user's changes disappeared. Locking mechanisms exist primarily to prevent such situations.
- Concurrency Is Usually A Business Problem
Concurrency issues rarely present themselves as obvious technical failures. Systems continue running, databases remain available, and monitoring dashboards look healthy. The real impact appears in business outcomes.
Customers do not care whether the root cause involves MVCC, transaction isolation levels, or a particular locking strategy. They only see incorrect results. For that reason, concurrency control is not merely a database concern—it is a business requirement that directly affects customer trust and operational correctness.
- Contention Changes Everything
Many applications operate flawlessly until contention increases. A user profile system may rarely experience concurrent updates because different users modify different records. In contrast, a payment platform may process thousands of updates against the same accounts every second. Similarly, a seat reservation system may have thousands of users competing for a very small number of records.
The frequency of contention is one of the most important factors when choosing a concurrency strategy. Systems with frequent conflicts require a different approach than systems where conflicts are rare. This distinction forms the foundation of the optimistic versus pessimistic locking debate.
2. Pessimistic Locking: Assume Conflict Will Happen
Pessimistic locking starts with a conservative assumption:
Someone else will probably try to modify this data.
Because conflicts are expected, the system prevents them from occurring by restricting access immediately. The first transaction acquires a lock on the data, and any subsequent transaction attempting to modify the same data must wait until the lock is released.
This approach prioritizes correctness by ensuring that only one transaction can modify a resource at a time.
- The Bank Account Example
Imagine two transactions attempting to modify the same account balance.
Transaction A begins and acquires a lock:
SELECT * FROM account WHERE id = 100 FOR UPDATE;
The row becomes locked, preventing other transactions from modifying it.
Now Transaction B attempts the same operation:
SELECT * FROM account WHERE id = 100 FOR UPDATE;
Because Transaction A already holds the lock, Transaction B cannot proceed. It must wait until Transaction A completes and releases the lock. This guarantees that updates occur sequentially rather than concurrently.
- What Happens Under Contention
The flow looks like this:
Notice that the second transaction does not fail. Instead, it pauses until the lock becomes available. This behavior makes correctness easier to reason about because the database itself enforces exclusive access to the data. Developers do not need to detect conflicts later because the database prevents them from occurring in the first place.
- Why Financial Systems Like Pessimistic Locking
Certain domains prioritize correctness above all else. Examples include payment processing systems, banking platforms, trading applications, and inventory reservation systems.
In these environments, waiting is preferable to risking inconsistent data. Consider two users attempting to reserve the last available airline seat. Allowing both requests to proceed simultaneously could result in overselling the seat, creating operational and customer-service problems. A short delay is usually a much smaller cost than correcting inconsistent business data later.
- The Cost Of Waiting
While pessimistic locking provides strong protection against conflicting updates, it introduces a different challenge: reduced concurrency.
As contention increases:
- response times increase
- throughput decreases
- blocked transactions accumulate
Under heavy load, lock contention can become a significant bottleneck. Instead of processing business operations, the database spends more time coordinating access to shared resources. This trade-off becomes increasingly visible in high-traffic systems where many users compete for the same records.
3. Optimistic Locking: Assume Conflict Is Rare
Optimistic locking takes the opposite approach.
Most transactions will not conflict.
Instead of preventing concurrent access, the system allows multiple users to work with the same data simultaneously. Rather than blocking access upfront, conflicts are detected later when an update is attempted.
This approach assumes that contention is relatively uncommon and that most operations can proceed without interference.
- The Core Idea
Optimistic locking typically relies on a version number stored alongside each record.
Example:
Account
--------
Id = 100
Balance = 1000
Version = 5
Suppose two users read the same row. Both receive:
Version = 5
User A updates the record first:
UPDATE account SET balance = 900, version = 6
WHERE id = 100 AND version = 5;
The update succeeds because the version matches the expected value. The record now becomes:
Version = 6
Later, User B attempts an update:
UPDATE account SET balance = 800, version = 6
WHERE id = 100 AND version = 5;
This update affects zero rows because the version is no longer 5. The database detects that another transaction modified the record first, and the update fails.
- Conflict Becomes Explicit
Unlike pessimistic locking, optimistic locking does not force transactions to wait. Instead, conflicting updates fail immediately.
The application must then decide how to respond. Common options include:
- retry
- refresh data
- reject the operation
- ask the user to resolve the conflict
This approach makes conflicts visible rather than hiding them behind waiting transactions. The responsibility for handling those conflicts shifts from the database to the application.
- Why Modern Applications Prefer Optimistic Locking
Many business applications experience relatively low contention. Examples include customer profiles, employee records, product catalogs, and content management systems. Most users interact with different records, making simultaneous updates uncommon.
In these environments, blocking every update would introduce unnecessary overhead. Optimistic locking allows the system to maximize concurrency while still detecting the occasional conflict. As a result, applications achieve better scalability and responsiveness.
- The Cost Of Retrying
Optimistic locking reduces database contention but introduces complexity elsewhere. Because conflicts are detected after they occur, applications must implement strategies for handling failures.
Retries may sound straightforward, but production systems require additional considerations such as exponential back-off,
user experience, duplicate submissions and retry storms.
As a result, conflict resolution becomes an important part of application design rather than a purely database-level concern.
4. How Modern RDBMS Actually Handle Concurrency
Many engineers imagine databases constantly locking rows and blocking transactions. Modern relational databases are far more sophisticated.
Systems such as PostgreSQL and MySQL rely heavily on a technique called Multi-Version Concurrency Control (MVCC). Understanding MVCC helps explain why modern databases can support high levels of concurrency without excessive blocking.
- Multiple Versions Of Data
Instead of immediately replacing existing data, MVCC creates new versions of rows whenever updates occur.
Conceptually:
┌───────────────┐
│ Row Version 1 │
└───────────────┘
↓
┌───────────────┐
│ Row Version 2 │
└───────────────┘
↓
┌───────────────┐
│ Row Version 3 │
└───────────────┘
Older versions remain available for active transactions that still need them. This allows readers to continue accessing a consistent view of the data while updates occur in parallel.
The result is significantly less blocking and much higher concurrency.
- Why Reads Usually Don't Block Writes
One of the most common misconceptions about databases is:
Every update blocks every read.
In MVCC-based databases, this is not true. Readers can access a consistent snapshot of the data while writers create newer versions in the background.
This capability allows databases to support large numbers of concurrent users without forcing readers and writers to constantly wait for one another. It is one of the primary reasons modern relational databases scale far better than many developers initially expect.
- Isolation Levels Matter
Locking strategies are only one part of the consistency story. Isolation levels determine what data a transaction can see while other transactions are running.
Common isolation levels include:
- Read Committed
- Repeatable Read
- Serializable
Each level provides different guarantees and trade-offs. Higher isolation levels generally offer stronger consistency but require additional coordination and overhead.
Choosing a locking strategy without understanding transaction isolation can lead to incorrect assumptions about application behavior. In practice, consistency emerges from the combination of locking mechanisms, MVCC behavior, and transaction isolation working together.
5. Deadlocks: The Hidden Cost of Pessimistic Locking
Pessimistic locking guarantees exclusive access to data by preventing multiple transactions from modifying the same resource simultaneously. While this approach is highly effective at preserving consistency, it introduces a different class of concurrency problems: deadlocks.
Deadlocks typically do not appear during initial development or testing because contention levels are low and transaction flows are relatively simple. As systems grow, however, more users, background processes, and business workflows begin interacting with the same data concurrently. Under these conditions, transactions may start waiting on each other in ways that create circular dependencies.
When that happens, transactions that previously completed successfully begin failing unexpectedly, without any changes to the underlying business logic.
- A Classic Deadlock Scenario
Consider a money transfer workflow involving two accounts.
Transaction A Transaction B
───────────── ─────────────
Lock Account A Lock Account B
│ │
▼ ▼
Update Account A Update Account B
│ │
▼ ▼
Lock Account B ◄──────────────► Lock Account A
DEADLOCK
Transaction A holds a lock on Account A and waits for Account B. Meanwhile, Transaction B holds a lock on Account B and waits for Account A.
Neither transaction can proceed because each will be waiting for a resource currently held by the other. Neither transaction can release its lock because it has not yet completed.
The database detects this circular wait condition and identifies it as a deadlock.
Deadlocks are not limited to two rows or two transactions. In complex systems, deadlocks may involve multiple tables, indexes, and transactions, making them difficult to diagnose without proper monitoring and logging.
- How Databases Resolve Deadlocks
Modern relational databases continuously analyze lock dependencies between active transactions. When a deadlock is detected, the database must break the cycle to allow progress.
A simplified flow looks like this:
┌───────────────┐
│ Transaction A │
└───────────────┘
↓
┌───────────────┐
│ Deadlock │
└───────────────┘
↓
┌──────────────────────────┐
│ Database Chooses Victim │
└──────────────────────────┘
↓
┌───────────────┐
│ Rollback │
└───────────────┘
The database selects one transaction as the deadlock victim and rolls it back. The other transaction is allowed to continue and eventually commit.
The victim selection process varies by database implementation. Factors such as transaction age, resource consumption, and rollback cost may influence which transaction is terminated.
From the application's perspective, this usually appears as an exception indicating that the transaction failed due to a deadlock. The application must be prepared to retry the operation because deadlocks are considered transient failures rather than permanent errors.
Importantly, deadlocks are not database bugs. They are an expected consequence of concurrent transactions acquiring locks in different orders.
- Deadlocks Become Operational Problems
Deadlocks are difficult to reproduce in development environments because concurrency levels are significantly lower than in production.
Real-world systems contain many independent actors operating simultaneously like concurrent users, background jobs, asynchronous consumers and scheduled tasks.
Each of these components may access shared resources using different execution paths.
A deadlock occurring once every few weeks may have little operational impact. However, when contention increases and deadlocks begin occurring hundreds or thousands of times per hour, they can significantly affect throughput, latency, and user experience.
For this reason, high-scale systems attempt to minimize lock durations, enforce consistent lock acquisition ordering, or adopt optimistic concurrency strategies when contention remains relatively low.
6. Optimistic Locking in Spring and JPA
Optimistic locking is one of the most commonly used concurrency control mechanisms in enterprise Java applications. Frameworks such as JPA and Hibernate provide built-in support, making implementation straightforward while still offering strong protection against lost updates.
Unlike pessimistic locking, optimistic locking does not prevent concurrent access. Instead, it detects whether another transaction modified the data between the time it was read and the time it was updated.
- The
@VersionAnnotation
A typical entity might look like:
@Entity
public class Account {
@Id
private Long id;
private BigDecimal balance;
@Version
private Long version;
}
The @Version field acts as a concurrency token. Every successful update increments the version number automatically.
When Hibernate generates update statements, it includes the current version value in the WHERE clause. This ensures that updates only succeed if the record has not been modified since it was originally read.
This mechanism allows multiple users to read the same data concurrently while still preventing silent overwrites.
- What Actually Happens
Suppose two users load the same entity.
Both receive:
Version = 10
User A updates first.
The version becomes:
Version = 11
User B attempts an update.
Hibernate generates an update statement similar to:
UPDATE account SET balance = ?, version = 11
WHERE id = ? AND version = 10
Because the row now contains version 11 instead of version 10, the WHERE condition no longer matches.
As a result, no rows are updated.
Hibernate detects this condition and throws OptimisticLockException. This exception indicates that another transaction modified the entity after it was originally loaded.
Rather than silently overwriting data, the application is forced to acknowledge and handle the conflict.
- Handling Optimistic Lock Failures
Adding @Version annotation is only the first step.
The more important challenge is deciding how the application should respond when conflicts occur.
Possible strategies include:
- retry automatically
- reject the operation
- reload and merge
- notify the user
The appropriate choice depends heavily on business requirements.
For example, inventory/reservation systems retry automatically because conflicts are expected and transient. Collaborative editing systems may present users with merge options. Financial applications frequently reload the latest state and re-validate business rules before attempting another update.
Optimistic locking provides conflict detection. It does not provide conflict resolution. Designing an effective resolution strategy is a critical part of building reliable systems.
7. Locking in NoSQL Databases
A common misconception is that NoSQL databases eliminate concurrency concerns.
In reality, concurrent modification problems still exist. The difference lies in how databases expose consistency guarantees and concurrency control mechanisms.
Most NoSQL platforms provide some form of optimistic concurrency control rather than traditional row-level locking.
- MongoDB
MongoDB provides atomic operations at the document level. Updates to a single document are isolated and executed atomically.
For concurrency control, many applications implement version-based optimistic locking.
Example:
db.orders.updateOne(
{
_id: 1,
version: 5
},
{
$set: {
status: "SHIPPED"
},
$inc: {
version: 1
}
}
)
The update succeeds only if the document still contains version 5.
If another process updates the document first, the query condition no longer matches:
Matched Documents = 0
The application can then detect the conflict and decide whether to retry or reject the operation.
Conceptually, this is very similar to optimistic locking in relational databases.
- Redis
Redis is generally viewed as a simple in-memory cache, but it is also frequently used as a primary data store, coordination mechanism, and distributed locking platform.
Because Redis executes commands sequentially within a single-threaded event loop, individual commands are atomic. However, concurrency challenges still arise when multiple clients perform read-modify-write operations.
One approach is to use optimistic concurrency control through the WATCH command.
Example:
WATCH account:100
GET account:100
The client reads the value and prepares an update.
When the transaction executes:
MULTI
SET account:100 900
EXEC
Redis verifies that the watched key has not changed since it was read.
If another client modifies the key before EXEC, the transaction is aborted: Transaction Failed
The application can then retry using the latest value.
Redis is also widely used for distributed locking through commands such as:
SET resource-lock unique-id NX PX 30000
This creates a lock only if the key does not already exist and automatically expires it after a specified timeout.
While distributed locks can co-ordinate access across multiple application instances, they should be used carefully. Improper lock expiration settings, network partitions, and process failures can introduce subtle consistency issues.
For this reason, many Redis-based systems prefer optimistic concurrency patterns or idempotent operations whenever possible, reserving distributed locks for workflows that truly require exclusive access.
- DynamoDB
DynamoDB provides optimistic concurrency control through conditional writes.
A write operation can specify a condition that must evaluate to true before the update is applied.
The following example performs an UpdateItem operation. It tries to reduce the Price of a product by 75—but the condition expression prevents the update if the current Price is less than or equal to 500.
aws dynamodb update-item \
--table-name ProductCatalog \
--key '{"Id": {"N": "456"}}' \
--update-expression "SET Price = Price - 75" \
--condition-expression "Price > 500"
If the starting Price is 650, the UpdateItem operation reduces the Price to 575. If you run the UpdateItem operation again, the Price is reduced to 500. If you run it a third time, the condition expression evaluates to false, and the update fails.
This approach allows DynamoDB to maintain high scalability while still preventing lost updates. Because conditional writes are implemented directly by the storage engine, applications can enforce concurrency guarantees without introducing explicit locking mechanisms.
Many large-scale AWS systems rely heavily on this pattern.
8. Distributed Systems Change Everything
Many engineers discover an uncomfortable reality when transitioning from monolithic applications to microservices:
Database locking does not extend beyond a single database.
Traditional locking mechanisms work extremely well within a single transactional boundary. Once data and business processes span multiple services, those guarantees disappear.
- Locks Cannot Cross Services
Consider:
Order Service
|
Database A
and
Inventory Service
|
Database B
A lock acquired in Database A has no effect on Database B.
Even if both services participate in the same business workflow, neither database has visibility into the other's locks or transactions.
As a result, traditional database locking cannot guarantee consistency across service boundaries.
This limitation fundamentally changes how distributed systems are designed.
- Why SAGAs Exist
Microservices frequently execute workflows that span multiple services and databases.
Example:
Create Order
│
▼
Reserve Inventory
│
▼
Process Payment
│
▼
Create Shipment
No single ACID transaction can encompass the entire workflow.
Instead, systems rely on:
- compensating transactions
- retries
- eventual consistency
This is the problem Saga patterns address.
Rather than locking resources across services, Sagas coordinate a sequence of local transactions and define recovery actions when failures occur.
The goal is not immediate consistency but reliable business outcomes despite partial failures.
- Why Outbox Doesn't Require Locks
The Transactional Outbox pattern solves a different challenge.
It guarantees Database Commit + Event Publication without requiring distributed transactions.
The application writes both business data and an outbound event record within the same local transaction. A separate process later publishes the event.
This approach relies on transactional guarantees within a single database.
Not pessimistic locking.
Understanding this distinction is important because many distributed systems problems are fundamentally reliability and coordination problems rather than concurrency-control problems.
- Idempotency Beats Locking
Many distributed systems avoid locking altogether.
Instead, they make operations idempotent, meaning the same operation can be executed multiple times without changing the final outcome.
Example:
Process Payment Event
The consumer records Payment Already Processed and ignores duplicates.
This strategy allows systems to safely retry operations without introducing global locks or distributed coordination.
Modern event-driven architectures frequently prefer:
- retries
- idempotency
- eventual consistency
over distributed locking because these approaches scale more effectively and remain resilient during failures.
9. Choosing Between Optimistic and Pessimistic Locking
Neither optimistic nor pessimistic locking is universally superior.
The correct choice depends on workload characteristics, contention frequency, consistency requirements, and performance goals.
Understanding how often conflicts occur is usually more important than understanding the locking mechanism itself.
- Choose Pessimistic Locking When
Pessimistic locking is most appropriate when conflicts are common and the cost of inconsistency is high.
Scenarios like:
- seat reservation systems
- inventory allocation
- financial transactions
- account balance updates
In these scenarios, allowing concurrent modifications may create unacceptable business outcomes. Waiting for access is preferable to resolving conflicts after they occur.
Correctness takes priority over throughput.
- Choose Optimistic Locking When
Optimistic locking works best when conflicts are relatively rare.
Scenarios like:
- customer profiles
- product catalogs
- employee records
- content management systems
Most transactions complete successfully without interference from other users. Because contention is low, avoiding locks improves concurrency and reduces database overhead.
The occasional conflict can be handled through retries or user intervention.
- Measure Contention First
Many teams choose a locking strategy based on assumptions rather than evidence.
A better approach is to measure:
- lock wait time
- retry rates
- update conflicts
- transaction latency
Production metrics reveal surprising patterns.
A workflow that appears highly contentious may rarely experience conflicts, while seemingly independent operations may compete heavily for shared resources.
Data should drive concurrency decisions whenever possible.
10. Common Mistakes Teams Make
Concurrency control is generally misunderstood because systems behave correctly under low load and fail only when contention increases.
Several mistakes appear repeatedly across production systems.
- Using Pessimistic Locking Everywhere
Applying pessimistic locking indiscriminately can severely limit scalability.
The application remains correct, but:
- throughput decreases
- latency increases
- lock contention grows
As traffic increases, the database spends more time coordinating access than executing business logic.
Correctness is essential, but excessive locking can become a significant performance bottleneck.
- Ignoring Retry Logic
Optimistic locking assumes conflicts will occasionally occur. Without retry mechanisms, users may experience unnecessary failures even when a simple retry would succeed immediately.
Applications should treat optimistic lock exceptions as expected outcomes rather than exceptional situations. Proper retry policies are as important as the locking strategy itself.
- Long Transactions
Locks held for extended periods dramatically increase contention. Transactions should perform only the work necessary to maintain consistency.
External API calls, file processing, and lengthy computations should generally occur outside transactional boundaries whenever possible.
Short transactions reduce lock duration and improve overall system throughput.
- Confusing Isolation Levels with Locking
Many developers assume Serializable automatically solves every concurrency problem.
In reality, isolation levels define visibility rules between transactions, while locking strategies define how concurrent modifications are co-ordinated.
Both influence consistency.
Neither replaces the other.
Understanding the distinction is critical when diagnosing concurrency issues.
11. Final Thoughts
Concurrency control is fundamentally the discipline of managing contention while preserving correctness.
Optimistic and pessimistic locking approach this challenge from different perspectives.
The correct choice depends on:
- contention patterns
- consistency requirements
- throughput goals
- operational behavior
Many production systems use both approaches simultaneously. Critical workflows may require strict exclusivity, while less contentious operations benefit from maximum concurrency.
The most effective engineers understand the trade-offs behind each strategy and apply them deliberately based on business requirements and real-world traffic patterns. Because concurrency problems rarely appear when systems are idle. They appear when traffic grows, users increase, and contention finally arrives.
Assisted AI to generate charts and diagrams.

Top comments (0)