DEV Community

Yashika Vijayvargiya
Yashika Vijayvargiya

Posted on • Originally published at railswithyashika.hashnode.dev

Part 3: Optimistic Locking in Rails – Preventing Lost Updates Without Blocking Users

Introduction

In the previous article, we learned how Pessimistic Locking prevents concurrent modifications by locking database rows.

While this guarantees data consistency, it also means other users may have to wait until the lock is released.

But what if conflicts are rare?

Should we still lock every row?

Probably not.

This is where Optimistic Locking comes in.

Instead of preventing concurrent updates, Optimistic Locking allows multiple users to edit the same record and detects conflicts only when they try to save their changes.

It assumes that conflicts are unlikely, making it a great choice for many web applications.

What is Optimistic Locking?

Definition

Optimistic Locking is a concurrency control strategy that allows multiple transactions to read and modify the same record without acquiring database locks.

Before saving changes, Rails checks whether the record has been modified by another transaction.

If it has, Rails raises an exception instead of silently overwriting the newer data.

In simple words:

"I believe no one else will modify this record. But before saving, I'll verify that assumption."

Why Do We Need Optimistic Locking?

Imagine an admin panel.

Two administrators open the same product.

Current Product:

Title: iPhone 16
Price: ₹80,000
Enter fullscreen mode Exit fullscreen mode

Admin A changes:

Price → ₹75,000
Enter fullscreen mode Exit fullscreen mode

Admin B changes:

Title → iPhone 16 Pro

Enter fullscreen mode Exit fullscreen mode

Without optimistic locking:

Admin A saves.

Database:

Title: iPhone 16
Price: ₹75,000
Enter fullscreen mode Exit fullscreen mode

A few seconds later...

Admin B saves.

Database becomes:

Title: iPhone 16 Pro
Price: ₹80,000
Enter fullscreen mode Exit fullscreen mode

Admin A's update is lost.

This is called a Lost Update.

How Rails Solves This

Rails provides built-in support for Optimistic Locking using a column named:

lock_version

Enter fullscreen mode Exit fullscreen mode

Whenever a record is updated:

  • Rails increments lock_version
  • Rails verifies the previous version before updating
  • If the version has changed, Rails raises an exception

Enabling Optimistic Locking

Simply add a column:

class AddLockVersionToProducts < ActiveRecord::Migration[8.0]
  def change
    add_column :products,
               :lock_version,
               :integer,
               default: 0,
               null: false
  end
end

Enter fullscreen mode Exit fullscreen mode

That's it.

Rails automatically enables optimistic locking.

No additional configuration is required.

Example

Current row:

Admin A loads:

lock_version = 0

Enter fullscreen mode Exit fullscreen mode

Admin B also loads:

lock_version = 0

Enter fullscreen mode Exit fullscreen mode

Admin A updates:

product.price = 75000
product.save!
Enter fullscreen mode Exit fullscreen mode

Database:

Notice:

lock_version

Enter fullscreen mode Exit fullscreen mode

became

1

Enter fullscreen mode Exit fullscreen mode

Now Admin B tries:

product.title = "iPhone Pro"

product.save!
Enter fullscreen mode Exit fullscreen mode

Rails generates SQL similar to:

UPDATE products SET title = 'iPhone Pro', lock_version = 2 WHERE id = 1 AND lock_version = 0;
Enter fullscreen mode Exit fullscreen mode

But the database contains:

lock_version = 1

Enter fullscreen mode Exit fullscreen mode

Therefore:

0 rows updated

Enter fullscreen mode Exit fullscreen mode

Rails raises:

ActiveRecord::StaleObjectError

Enter fullscreen mode Exit fullscreen mode

Timeline

Admin A

Read Product

Version = 0

↓

Edit

↓

Save

Version becomes 1

----------------------------

Admin B

Read Product

Version = 0

↓

Edit

↓

Try Save

↓

StaleObjectError
Enter fullscreen mode Exit fullscreen mode

What Happens Internally?

Suppose current version:

lock_version = 3

Enter fullscreen mode Exit fullscreen mode

Rails updates using:

UPDATE products
SET
price = 100,
lock_version = 4
WHERE
id = 1
AND lock_version = 3;
Enter fullscreen mode Exit fullscreen mode

If another transaction already updated it:

lock_version = 4

Enter fullscreen mode Exit fullscreen mode

The WHERE condition fails.

No row is updated.

Rails knows someone modified the record.

Handling the Exception

Typical implementation:

begin
  product.update!(product_params)

rescue ActiveRecord::StaleObjectError

  flash[:alert] =
    "This product was updated by another user. Please reload and try again."

  redirect_to edit_product_path(product)
end
Enter fullscreen mode Exit fullscreen mode

Instead of silently overwriting data, the user is informed.

Real Production Example 1

CMS Article Editing

Two editors modify the same article.

Without optimistic locking:

One editor overwrites another's work.

With optimistic locking:

Second editor sees:

"This article has changed since you opened it."

Enter fullscreen mode Exit fullscreen mode

Real Production Example 2

Admin Dashboard

Inventory manager changes:

Stock = 15

Enter fullscreen mode Exit fullscreen mode

Manager B changes:

Price = ₹500

Enter fullscreen mode Exit fullscreen mode

Instead of losing one update,

Rails detects the conflict.

Real Production Example 3

User Profile

Editing profile:

  • Name

  • Bio

  • Address

Conflicts are rare.

Blocking users would hurt UX.

Optimistic locking is a better fit.

Advantages

✅ No waiting

✅ Better scalability

✅ Better user experience

✅ No database locks

✅ Great for read-heavy applications

Disadvantages

❌ Save may fail

❌ Users must retry

❌ Not suitable for financial systems

❌ Doesn't prevent concurrent reads

Optimistic vs Pessimistic Locking

Top comments (0)