Goal of this article: Build a strong foundation before learning pessimistic locking, optimistic locking, deadlocks, and isolation levels.
When I started working with production Rails applications, I realized that most database problems were not caused by missing indexes but by concurrent requests modifying the same data. Understanding locking completely changed the way I design payment, inventory, and booking systems.
What is a lock?
A lock is a mechanism used by PostgreSQL to protect data from being modified incorrectly by multiple transactions at the same time.
In simple words:
A lock tells other transactions: “Someone is currently working with this data. Wait your turn.”
Imagine two people editing the same Google Doc at the same moment. Without coordination, one person’s changes could overwrite the other’s. Databases use locks to prevent similar problems.
Why do databases need locking?
Suppose an e-commerce application has only 1 item left in inventory.
Without locking, both requests may read:
Both transactions believe the product is available and both place an order.
Result:
This is called a race condition.
What is a race condition?
A race condition occurs when the final result depends on the timing of concurrent operations.
Example
If two requests execute this code simultaneously, both may read the same inventory value before either update occurs.
This is the fundamental problem locking is designed to solve.
Transactions: The Foundation of Locking
Locks exist inside database transactions.
A transaction guarantees that all operations inside the block either:
- Succeed together (COMMIT)
- Fail together (ROLLBACK)
Locks are typically acquired when the transaction begins modifying or explicitly locking rows.
How PostgreSQL Handles Concurrent Transactions
Consider two transactions.
PostgreSQL automatically prevents both transactions from modifying the same row simultaneously.
MVCC: Why SELECT Usually Doesn’t Block
One of PostgreSQL’s most important features is MVCC (Multi-Version Concurrency Control).
Instead of blocking readers, PostgreSQL creates multiple versions of rows.
Example
Transaction A updates a product but has not committed yet.
At the same time, Transaction B runs:
Transaction B usually does not wait. It reads the previous committed version of the row.
This is why PostgreSQL can handle many readers efficiently.
Common Concurrency Problems
1. Lost Update
Very common
Problem: Two users overwrite each other’s changes.
Correct result should be: $30
Actual result: $50
2. Dirty Read
Usually prevented in PostgreSQL
Reading data that another transaction has modified but not committed.
PostgreSQL’s default isolation level (Read Committed) prevents this.
3. Non-repeatable Read
Possible in Read Committed
You read the same row twice and get different values because another transaction committed between the reads.
4. Phantom Read
Isolation-level issue
A query returns a different set of rows the second time it runs because another transaction inserted matching records.
Rails Example: Preventing Double Booking
Unsafe code:
Safe code using locking:
What changed?
generates:
This acquires a row-level lock so only one transaction can modify that event record at a time.
Visual Timeline
Important Insight: PostgreSQL Uses Multiple Lock Types
Many beginners think there is only one lock.
In reality PostgreSQL has:
We’ll explore these deeply in later parts of the series.
How to See Locks in Production
PostgreSQL exposes lock information through pg_locks.
A more useful query:
This helps identify which transaction is holding a lock and which transaction is waiting.
Key Takeaways
Locks prevent race conditions and data corruption.
Locks exist inside transactions.
PostgreSQL uses MVCC so readers usually do not block writers.
Row locks protect individual records.
Table locks protect entire tables.
FOR UPDATE is the foundation of pessimistic locking in Rails.
Most production concurrency bugs are caused by missing transactions or missing locks.
Interview Question
Why does PostgreSQL allow many SELECT queries to run without blocking each other?
Answer: PostgreSQL uses MVCC (Multi-Version Concurrency Control). Instead of blocking readers, it keeps multiple versions of rows so a SELECT query can read the last committed version while another transaction is updating the row. This greatly improves concurrency and is one of PostgreSQL’s biggest advantages.
Next Article in the Series
Pessimistic Locking in Rails
We will dive into lock, with_lock, FOR UPDATE, NOWAIT, SKIP LOCKED, and real production examples such as inventory reservation and payment processing.
Originally published at https://railswithyashika.hashnode.dev on July 13, 2026.






Top comments (0)