Locks Are Not Syntax, They Are Boundaries
A practical explanation of what locks are for, when to consider them, how optimistic and pessimistic locking differ, and why local locks are not enough in distributed systems.
Imagine a paper sign-out sheet sitting by the office door.
Who borrowed the projector, who took the meeting room key, who picked up the last box of printer paper: everyone has to write it down. When only one person is writing, everything is fine. Name, time, item. Done.
The trouble begins when two people try to write on the same line.
One person has written a name but not the time yet. Another person crosses out that line and writes their own record. A third person sees that the sheet still says one box of printer paper is available and takes it away. In the end, all three people feel they did nothing wrong, but the ledger is already a mess.
That is the problem locks are meant to solve.
A lock is not some advanced piece of language syntax. It is not there to make code look more “senior”. It is first of all a boundary: when many people may modify the same thing at the same time, the system needs a way to decide who goes first, who waits, how failure is handled, and what final result counts as correct.
This idea is worth revisiting now that many people write code with AI.
AI can quickly produce APIs, forms, validation, database access, and tests. It will also become better at deciding where a lock is needed, where a transaction is needed, and where idempotency is needed. But developers still need to understand why it made those choices.
If it adds a lock, you need to know whether it locked the right resource.
If it does not add a lock, you need to know whether this place really does not need one.
If it uses a distributed lock, you need to know whether that lock is truly visible to all instances, or whether it only locked the door of one small room.

Chinese version of this article
Locks Solve Disorder, Not Slowness
When people first learn about locks, they often connect them with performance.
That is understandable. Locks often make programs slower. One person gets the lock, others wait. The larger the lock scope, the more people wait. The longer the lock is held, the more throughput falls.
But the first problem a lock solves is not speed. It solves correctness.
Without a lock, or without another form of concurrency control, a program may run very fast. It may simply write bad data very fast.
In the paper sign-out sheet example, nobody has to wait. That is certainly the fastest version. The cost is that nobody knows who actually borrowed the projector. Meeting room reservations are similar. The system shows that 2 p.m. is available. Two teams click reserve at the same time. If there is no constraint in between, the same room may be assigned to both teams.
This is not just a display bug.
In a real system, that sheet may be an inventory table, an account balance row, an order status row, a coupon redemption record, or a task queue table. These records can be read and modified. Once multiple requests modify the same data at the same time, the system needs an answer.
Who modifies first?
Who modifies later?
If the later request discovers that the data has changed, should it retry, fail, or reload?
A lock is one of the most common answers.
When to Think About Locks
Do not add a lock the moment you see the word “concurrency”.
A steadier way is to ask three questions.
First, can multiple execution units happen at the same time?
An execution unit is not only a Java thread. It can be two HTTP requests, two background jobs, two service instances, two message consumers, or two users clicking the same button in the same second.
Second, do they access the same resource?
The same product, the same account, the same meeting room, the same coupon, the same task record: all of these count.
Third, is there a modification?
Pure reads are usually easier. The trouble starts when at least one side writes. Two requests checking whether a meeting room is free are not the problem. Two requests both changing the same room to “reserved” are.
If all three answers are yes, you should consider concurrency control.
Consider concurrency control, not immediately hand-write a lock.
Some cases are suited to locks. Some are better handled by a database unique index. Some only need a conditional update. Some should be turned into queue-based serial processing. A lock is a tool. Not every door needs to become a vault door.
What Is Actually Being Locked
Beginners often think a lock locks a piece of code.
For example, in Java, synchronized looks as if it locks a method or a code block. That description is convenient, but it can mislead.
The real question is: what resource is this lock protecting?
If it protects inventory, then decrementing the inventory of the same product should be mutually exclusive. Different products usually do not need to wait for each other.
If it protects an account balance, then deductions and deposits on the same account need care. Different accounts usually should not line up in one long queue.
If it protects a meeting room, then Room A and Room B can be reserved independently. Room B should not be blocked just because Room A is locked.
Lock granularity matters.
If the lock is too small, it does not protect the problem. You meant to protect one account balance, but you only locked a temporary object. Another service instance can still update the database.
If the lock is too large, the system gets jammed. If one product’s inventory update blocks every product in the shop, that is like cutting power to an entire building so two people do not fight over one pen.
So more locks are not automatically safer. Larger locks are not automatically safer either.
A good lock protects exactly the resource that can be written incorrectly.
The Smallest Example: count++
Technical articles often use count++ to explain locks. It is old-fashioned, but it works.
count++;
That line looks like one action. In reality, it can be understood as three steps:
read count
calculate count + 1
write count back
If two threads both read count = 10, each calculates 11, and both write 11 back, the final result is 11, not 12.
Inventory deduction, balance changes, and order status transitions are business versions of this same problem.
The difference is that if count++ is wrong, maybe one statistic is off by one. If a balance is wrong, it is no longer just a number being off by one.
The problem is not that this line of code is complicated.
The problem is that it pretends only one person exists in the world.
Pessimistic Locking: Close the Door First
Pessimistic locking is a plain idea: I believe conflict may happen, so I lock the resource first. When I finish, others can come in.
It is like entering a small room that can hold only one person. You close the door after entering. Not because someone will definitely rush in, but because if it happens, the result is ugly.
Java’s synchronized, ReentrantLock, and a database’s SELECT ... FOR UPDATE can all be understood through this idea.
A common database form looks like this:
SELECT balance
FROM account
WHERE id = ?
FOR UPDATE;
UPDATE account
SET balance = balance - ?
WHERE id = ?;
This usually runs inside one transaction. Before the transaction commits, that account row is locked. Other transactions that want to modify the same row have to wait.
Where does pessimistic locking fit?
It fits places with higher conflict, data that must not be wrong, and failures that are not easy to retry. Account deductions, critical order status transitions, and some strongly constrained resource reservations are typical examples.
Its cost is also direct.
Others have to wait. Holding a lock too long hurts throughput. Taking multiple locks in a messy order can cause deadlocks. A pessimistic lock is like temporarily closing a road. Useful when needed; painful when overused.
One practical rule is: do not do slow work while holding a lock.
In particular, avoid calling remote services while holding a database row lock. That is like locking everyone outside while you make a phone call inside the room. If the call lasts five minutes, the people outside will move from polite patience to questioning their life choices.
Optimistic Locking: Go First, Check the Ticket Later
Optimistic locking takes the opposite attitude.
It does not stop people in advance. Everyone works first. When a modification is submitted, the system checks whether the data changed in the meantime.
A common approach is a version column.
When updating inventory, include the old version:
UPDATE product
SET stock = stock - 1,
version = version + 1
WHERE id = ?
AND stock > 0
AND version = ?;
If the affected row count is 1, the update succeeded.
If it is 0, either inventory is insufficient or the row was already changed by someone else. The next step may be retrying, or it may be returning failure.
Optimistic locking fits low-conflict, read-heavy cases where failure and retry are acceptable: editing an article, updating configuration, or ordinary inventory deduction.
Its trouble is failure handling.
Many implementations only write the success path. What happens if the update fails? Retry how many times? What does the user see? Can retries make the system even busier?
Those questions cannot be skipped.
Optimistic locking is not “no lock”. It turns waiting into checking, and turns queuing into failure handling.
There is also a common term called ABA. It means data changed from A to B and then back to A. It looks unchanged, but something happened in the middle. In many business systems, an increasing version number is enough to avoid this. There is no need to start from the lowest-level details.
How to Choose
Pessimistic locking means closing the door first, then doing the work.
Optimistic locking means doing the work first, then checking the ticket.
Neither is more advanced. Comparing technologies without a scenario usually turns into a fight between terms. The terms may fight loudly. The production system does not care.
A rough guide:
| Question | More likely choice |
|---|---|
| High conflict and retry is expensive | Pessimistic locking |
| Low conflict and retry is acceptable | Optimistic locking |
| Data must not be wrong, such as balances | Transactions, row locks, ledgers, idempotency together |
| Only need to prevent duplicate creation | Unique indexes and idempotency keys are often more direct |
| Multi-instance deployment | Local locks are usually not enough; use external constraints |
In engineering, some answers really are “it depends”. What matters is not memorizing one fixed choice, but laying out the conditions.
How likely is conflict?
How bad is a wrong result?
Can the operation be retried?
Is the system single-process or multi-instance?
Where is the most reliable place to enforce the constraint?
Once these conditions are clear, the solution usually has a direction.

Meeting Rooms, Inventory, and Balances
The easiest everyday example is meeting room reservation.
The same meeting room at the same time can only be reserved by one team. The solution does not have to be a hand-written lock. A database unique constraint can do the job: room ID plus time range must not duplicate. Whoever writes first succeeds; the later write fails.
This is like saying the sign-out sheet cannot contain two rows with the same record number.
Inventory deduction is similar.
The easiest wrong version looks like this:
check inventory
if inventory is greater than 0
deduct inventory
Single-user testing passes. Under concurrency, two requests may both read stock = 1, both believe they can buy, and the product is oversold.
A more direct approach is to combine the check and the modification into one SQL statement:
UPDATE product
SET stock = stock - 1
WHERE id = ?
AND stock > 0;
If the affected row count is 1, the deduction succeeded.
If it is 0, inventory was insufficient.
This SQL is plain, almost boring, but useful. It puts the constraint “stock must be greater than 0” inside the database operation and avoids someone slipping in between “check” and “update”.
Balances are more sensitive.
Overselling inventory is ugly, but it can still be handled by restocking, refunding, and apologizing. If balances are wrong, the system loses a piece of trust. The scariest thing in an accounting-like system is not ugly code. It is books that do not reconcile.
For balance changes, thinking only about “adding a lock” is not enough. At least transactions, ledgers, idempotency, and unique indexes need to be considered.
A transaction makes the balance update and ledger insert succeed or fail together.
A ledger makes every change traceable.
Idempotency ensures the same deduction request is not executed twice because of a retry.
A unique index stops duplicate request IDs at the database boundary.
This shows an important point: concurrency control is not only locks.
A lock is one tool among several.
Duplicate Submission Is Not Just a Lock Problem
Users double-click buttons, browsers resend requests, clients retry after timeouts, and message queues redeliver messages. All of these can cause duplicate submission.
Take payment as an example.
A user clicks pay. The network stalls. They click again. Or the frontend sent only one request, but the gateway retried. If the system creates a new payment order for every request, the rest becomes lively in the worst possible way.
Many people first think of adding a lock.
Sometimes that works. It is not always the best answer.
The key point in duplicate submission is that the same business action should be processed only once. This is closer to idempotency.
The word idempotency sounds a little mathematical. In business systems, it can be understood simply: the same voucher may arrive several times, but only the first one counts; later arrivals return the same result.
Common approaches include:
- Disable the frontend button to improve experience, but do not treat it as backend protection.
- Use an
idempotency_keyor request ID on the backend. - Add a database unique index to block duplicate request IDs.
- Return the first processing result for duplicate requests.
- Use a short Redis lock to absorb very short repeated clicks if needed.
Locks prevent “entering at the same time”.
Idempotency prevents “doing the same thing multiple times”.
They are related, but they are not the same thing.
Repeated Jobs: The Door Is Not in the Same Room
In local development, a scheduled job usually runs in one process.
Production changes that.
Three instances may each have the same cron job. When the time comes, all three scan the database. You meant to process one batch of data, and it gets processed three times.
Java’s synchronized does not help here.
Each instance has its own JVM, memory, and locks. Machine A locks something; Machine B does not know.
A local lock is like the door lock of one room. In a multi-instance deployment, there are several rooms, each with its own lock. You locked your own door, while the room next door can still modify the same remote ledger.
This kind of situation needs a public gate that all instances can see.
Common choices:
- A Redis distributed lock, allowing only one instance to execute at a time.
- A database task table claim, where whoever updates the state successfully owns the task.
- A message queue, splitting work into messages consumed under queue rules.
- A scheduler that guarantees single-instance execution for the job.
The point is not to memorize a framework. The point is to know that local locks and distributed coordination are not the same thing.
A Distributed Lock Is Not a Luxury synchronized
When multi-instance deployment comes up, Redis distributed locks often come to mind.
They are useful, but do not think of them as a remote luxury version of synchronized.
Distributed locks have to face real problems:
- How long should the lock live?
- What if business execution exceeds the lock expiration time?
- Can the lock be released if the service crashes?
- Is renewal needed?
- Can releasing the lock delete someone else’s newer lock?
The basic requirement is that the lock has an expiration time, the lock value has a unique identity, and release first verifies that the current holder really owns the lock.
Otherwise, a strange situation can happen.
A acquires the lock. The business code runs too slowly, and the lock expires. B acquires a new lock and starts processing. A finally finishes and deletes B’s lock.
This is not folklore. It is an ordinary awkwardness that real systems can hit.
That is why in many cases, database unique indexes, conditional updates, or task-table claiming are steadier than a hand-written distributed lock.
A distributed lock can be used. But you need to know what you are holding.
State Transitions: Draw the Roads First
An order may move from “pending payment” to “paid”, then to “shipped”. It may also be refunded, canceled, or closed.
These states are not arbitrary.
Pending payment can become paid or canceled. After shipping, an order cannot pretend to go back to pending payment. Refunds are not allowed at every moment either.
Under concurrency, payment callbacks, user cancellations, admin shipping actions, and refund requests may arrive at the same time. If the final status becomes messy, reading logs starts to feel like archaeology.
Locks solve only part of this problem.
The deeper issue is state constraints.
A common pattern is a conditional update:
UPDATE orders
SET status = 'PAID'
WHERE id = ?
AND status = 'PENDING_PAYMENT';
Only when the order is still pending payment can it become paid. If the affected row count is 0, the status has already changed, and the current operation cannot pretend to have succeeded.
This is the plain version of a state machine.
A lot of bad code is too confident: set status = xxx, regardless of the old state.
In state transitions, a lock is a door, and the state machine is a road map. If you install doors but never draw the roads, people still get lost.
Different Languages Use Different Door Locks
Java has synchronized, ReentrantLock, and Atomic.
Go has sync.Mutex, sync.RWMutex, and often uses channels to turn shared modification into queue-like processing.
Python has threading.Lock, and async code has asyncio.Lock.
Node.js has an event-loop model on the main thread, but server-side systems still face concurrent requests, multiple processes, multiple instances, and database concurrency.
Do not be fooled by “Node is single-threaded”.
That sentence only means a piece of JavaScript code is not executed by two CPUs at the same time in the same thread. It does not stop two HTTP requests from updating the same database row, and it does not stop two machines from running the same job.
Language-level locks mostly solve in-process problems.
Business-system concurrency often happens between the database, cache, message queue, and multiple service instances.
So the first question is not which language you use.
The first question is: where is the shared data, where are the concurrent entrances, and who guarantees correctness?
Back to AI
AI will become better at writing code and planning solutions.
That is a good thing.
But developers cannot hand over basic concepts entirely. Whether a system breaks usually depends less on whether the code looks plausible and more on whether the constraints were placed in the right location.
If AI adds synchronized to an API and the service is deployed in multiple instances, you need to know that this lock may only protect the current JVM.
If AI writes “check first, then update” for inventory deduction, you need to know that concurrency can slip in between.
If AI adds a Redis lock for duplicate submissions, you still need to check whether there is an idempotency key and a unique index.
If AI directly overwrites an order status with PAID, you should ask: what was the old status, and is this transition legal?
This does not mean everyone has to memorize JVM, AQS, JMM, or volatile details. Those things matter, but not every piece of business code needs to drill down to the center of the earth.
The more common and more important questions are:
- Is there a shared resource?
- Is there concurrent modification?
- How bad is a wrong result?
- Can failure be retried?
- Is this single-process or multi-instance?
- Should the constraint live in code, the database, Redis, a queue, or the scheduler?
Once these questions are clear, you can tell whether the solution AI gives you is reliable.
If you cannot understand them, faster code generation can simply bring faster mistakes.
That is not meant to be scary. It is ordinary engineering sense: when the car gets faster, road signs and brakes have to keep up.
Finally
Locks solve correctness problems when shared resources are modified concurrently.
Pessimistic locking fits high-conflict cases where failure is hard to accept. Close the door first, then do the work.
Optimistic locking fits lower-conflict cases where retry is acceptable. Do the work first, then check the ticket.
In distributed systems, local locks are usually not enough. Database locks, Redis distributed locks, unique indexes, conditional updates, message queues, and task-table claiming may all be part of the answer.
In real business code, do not start by asking, “Should I add a lock?”
Ask first:
- Can this business flow be wrong under concurrency?
- How bad is the impact if it is wrong?
- Where is the best place to enforce the constraint?
- Which resource is the AI or framework solution actually locking?
A lock is not an advanced concept.
It simply reminds us that some doors in a system should not let everyone squeeze through at once.
As for which door to close, how long to close it, and whether to use a key, an access card, or a sign-out sheet, people still have to think that through.
Loading discussion...
Discussion failed to load. Reload