Imagine two users editing the same record at almost the exact same time.
Both users click Save. Both API requests return 200 OK. Everything appears to work correctly.
But there is a problem.
One user’s changes may overwrite the other user’s changes.
This is a common concurrency problem in modern applications. It can lead to lost updates, incorrect data, and unexpected results.
Understanding concurrency is important when building reliable APIs and database systems.
A Simple Example of a Concurrency Problem
Consider an e-commerce application where two administrators are editing the same product.
The database initially contains:
Laptop — ₹50,000
Both administrators open the product at the same time.
Admin A changes the price to:
₹48,000
Admin B changes the price to:
₹49,000
Both administrators click Save.
If the application simply processes both update requests, the request that reaches the database last may overwrite the earlier update.
The final price could become:
₹49,000
Admin A’s update has now disappeared.
This is called a lost update.
The application accepted both requests, but one user’s change was silently overwritten.
Why Does a Lost Update Happen?
Modern applications handle many requests at the same time.
A simplified request flow looks like this:
User → Frontend → API → Database
Now imagine two users sending requests almost simultaneously.
User A ──────┐
├──> API ──> Database
User B ──────┘
Both users may read the same original value before either update reaches the database.
For example:
Initial price: ₹50,000
Admin A reads: ₹50,000
Admin B reads: ₹50,000
Admin A changes the price to ₹48,000.
Admin B changes the price to ₹49,000.
If there is no concurrency control, the last update can overwrite the first one.
The database is not necessarily doing anything wrong.
The real problem is that the application has not defined how concurrent updates should be handled.
This situation is commonly known as a race condition.
What Is a Race Condition?
A race condition occurs when the result of an operation depends on the timing or order of multiple requests.
For example:
Request A reads data
Request B reads data
Request A updates data
Request B updates data
If both requests use the same old data, the second update may overwrite the first one.
The result can therefore depend on which request reaches the database first or last.
That makes the behavior difficult to predict.
Transactions: The First Layer of Protection
One important tool for handling database operations is a transaction.
A transaction groups multiple database operations into one logical unit.
Consider an e-commerce application selling the last available product.
The application may need to:
- Check the available stock.
- Decrease the stock.
- Create the order.
These operations should work together.
A simplified transaction looks like this:
BEGIN TRANSACTION
Check stock
Decrease stock
Create order
COMMIT
If something goes wrong, the application can roll back the transaction:
ROLLBACK
This means the related changes are undone instead of leaving the database in an incomplete state.
However, transactions alone do not automatically solve every concurrency problem.
When multiple requests modify the same record, the application may also need locking, optimistic concurrency, or database constraints.
Pessimistic Locking
Pessimistic locking takes a simple approach:
Lock the record before modifying it.
The idea is that if one transaction is already working with a record, another transaction should wait before making a conflicting change.
For example, Laravel provides lockForUpdate() for this type of operation.
DB::transaction(function () {
$product = Product::where('id', 1)
->lockForUpdate()
->first();
$product->price = 48000;
$product->save();
});
Here, the application starts a transaction and locks the selected database row.
Conceptually, the process looks like this:
User A
↓
Lock Row
↓
Update Row
↓
Commit
↓
Release Lock
User B
↓
Wait
↓
Continue
While User A holds the lock, User B may have to wait before performing a conflicting operation.
This approach can be useful when data must be updated safely and immediately.
Common examples include:
- Inventory updates
- Seat reservations
- Stock management
- Financial transactions
- Critical state changes
Optimistic Locking
Pessimistic locking is not always the best solution.
Another approach is optimistic locking.
Instead of locking the record immediately, the application checks whether the record changed after it was read.
For example, suppose the product contains:
Product: Laptop
Price: ₹50,000
Version: 3
Both administrators read version 3.
Admin A changes the price:
Price: ₹48,000
Version: 4
Admin B still has the old version:
Version: 3
When Admin B tries to save the changes, the application checks the version.
It sees that the database is already on version 4.
The application can reject the update instead of silently overwriting Admin A’s changes.
For example, the API could return:
409 Conflict
The frontend can then tell Admin B that the record has changed.
The user can refresh the record, review the latest data, and decide whether to make the change again.
How Does Optimistic Locking Work?
A common implementation uses a version column.
For example:
id | price | version
1 | 50000 | 3
The update query can check the version before changing the record.
Conceptually:
UPDATE products
SET price = 49000,
version = 4
WHERE id = 1
AND version = 3;
If another user has already changed the record, the version is no longer 3.
The update therefore affects zero rows.
The application can detect this and return a conflict response.
This prevents silent data loss.
Optimistic locking works especially well when conflicts are relatively uncommon.
Pessimistic vs. Optimistic Locking
Both approaches solve concurrency problems, but they work differently.
| Approach | How It Works | Best For |
|---|---|---|
| Pessimistic Locking | Locks the record during the transaction | High-conflict operations |
| Optimistic Locking | Detects changes before saving | Occasional conflicts |
| Transactions | Groups related operations together | Maintaining data consistency |
| Database Constraints | Prevents invalid data states | Enforcing business rules |
The right approach depends on the type of operation and the expected level of contention.
Where Does Concurrency Matter in Real Applications?
Concurrency problems are not limited to e-commerce applications.
They can appear in many systems.
Inventory
Suppose only one product remains in stock.
Two customers try to purchase it at the same time.
Without proper concurrency control, both orders could potentially succeed.
The inventory could then become incorrect.
Seat Booking
Imagine a cinema with one remaining seat.
Two users select the same seat.
Both requests reach the server at nearly the same time.
The application needs a reliable way to ensure that only one user gets the seat.
Banking
Banking systems must carefully handle simultaneous transactions.
Two operations could attempt to modify the same account balance.
Incorrect concurrency handling could result in an inaccurate balance.
Order Processing
An order may be updated by several services.
For example:
Payment Service
↓
Order Service
↓
Shipping Service
↓
Notification Service
If multiple processes update the same order at the same time, the application must prevent inconsistent states.
Admin Dashboards
Two administrators may edit the same configuration.
Without conflict detection, one administrator could unknowingly overwrite the other’s changes.
Collaborative Applications
Document editors and other collaborative systems can have many users modifying the same data.
These systems need more advanced concurrency strategies to decide how simultaneous changes should be handled.
Why Frontend Validation Is Not Enough
Consider an application that shows:
Stock: 1
Two users see the same value.
Both click Buy.
The frontend might perform a simple check:
if (stock > 0) {
purchase();
}
The problem is that frontend data can become stale.
Both users may see:
Stock: 1
But the first purchase could complete before the second request reaches the backend.
The second user’s frontend still thinks one item is available.
This is why critical business rules cannot depend only on frontend validation.
Frontend validation is useful for improving the user experience.
However, the backend and database must ultimately protect the data.
Backend and Database Controls Matter
A reliable application should enforce important rules on the server side.
Depending on the use case, this can involve:
- Transactions
- Row-level locking
- Optimistic locking
- Database constraints
- Unique indexes
- Atomic updates
- Validation
- Proper error handling
The frontend should provide useful feedback.
The backend should enforce the actual business rules.
The database should help maintain data integrity.
What About API Retries?
Concurrency becomes even more important when requests can be retried.
Imagine a user clicks Save.
The request reaches the server successfully.
However, the network connection fails before the frontend receives the response.
The frontend may assume the request failed and send it again.
Now the same operation may be processed twice.
This is why production systems also need to think about idempotency.
For operations such as payments and order creation, the application may use an idempotency key.
This helps ensure that retrying the same request does not accidentally create duplicate operations.
What Happens When 100 Requests Arrive at Once?
Testing with one user is not enough for production systems.
An API may work perfectly when one person updates a record.
But what happens when 100 requests arrive at almost the same time?
For example:
Request 1 ──┐
Request 2 ──┤
Request 3 ──┤
Request 4 ──┤
... ├──> API ──> Database
Request 99 ──┤
Request 100 ─┘
This is where concurrency testing becomes important.
Developers should test how the application behaves when multiple requests compete for the same resource.
Questions Developers Should Ask
When building an update API, do not only ask:
“Does the update work?”
Also ask:
- What happens if two users update the same record?
- What happens if 100 requests arrive simultaneously?
- What happens if the record changes between read and update?
- What happens if a request is retried?
- What happens if a transaction fails?
- What happens if two background workers process the same record?
- What happens if a database lock is held for too long?
- What should the user see when a conflict occurs?
- Can the system recover safely from a failed operation?
These questions help uncover problems that normal functional testing may not reveal.
The Production Mindset
In development, it is easy to focus on whether an API performs the expected operation.
Production systems require a broader perspective.
Developers must also consider timing, failures, retries, concurrent requests, and data consistency.
A good backend design should answer an important question:
What happens when multiple operations try to change the same data at the same time?
There should be a clear answer before the application reaches production.
Choosing the Right Concurrency Strategy
There is no single solution for every application.
The right strategy depends on the operation.
For example:
Use transactions when multiple database operations must succeed or fail together.
Use pessimistic locking when conflicting updates are likely and the operation must be protected while it runs.
Use optimistic locking when conflicts are less common but silent overwrites must be prevented.
Use database constraints when the database itself should prevent invalid states.
Use idempotency when requests may be retried and duplicate processing must be avoided.
In many production systems, these techniques work together.
The Key Takeaway
Concurrency is not an unusual edge case.
It is a normal part of modern applications.
Whenever multiple users, API requests, or background jobs can modify the same data, the application needs a clear concurrency strategy.
A reliable system may use:
Transactions + Locking + Optimistic Concurrency + Database Constraints + Idempotency
The goal is not to prevent users from working at the same time.
The goal is to make sure their simultaneous actions do not silently produce incorrect data.
So, the next time you build an update API, do not stop at:
“Does this endpoint work?”
Ask a more important question:
“What happens when two users update the same record at the same time?”
That question is often the starting point for building reliable backend systems.