Imagine a company that tracks inventory for an online store. Customers can add items to their cart, and the system ensures that the stock count is accurate. Multiple customers might try to purchase the same product at the same time.

For instance, a Product model has a stock attribute:

# app/models/product.rb
class Product < ApplicationRecord
  validates :stock, numericality: { greater_than_or_equal_to: 0 }
end

Two users view a product with a stock of 1. Both decide to purchase it simultaneously. Each process loads the product and checks the stock. Both processes see the stock is 1 and attempt to decrement it. The first process reduces the stock to 0 and saves the record. The second process, unaware of the first change, also saves the record, overwriting the stock with 0.

This results in incorrect data because the system allowed both purchases to go through.

Solution: Optimistic Locking

Optimistic locking ensures data consistency when multiple users or processes update the same record. Rails provides built-in support for this feature with the lock_version attribute.

Step 1: Add lock_version to the Model

Generate a migration to add a lock_version column to the products table.

rails generate migration AddLockVersionToProducts lock_version:integer
rails db:migrate

Rails automatically manages the lock_version column. Each time a record is updated, the lock_version is incremented.

Step 2: Enable Optimistic Locking

No additional configuration is needed. Rails will automatically raise an exception if the lock_version has changed during an update.

# app/models/product.rb
class Product < ApplicationRecord
  validates :stock, numericality: { greater_than_or_equal_to: 0 }
end

Step 3: Handle Conflicts in Code

When saving a record, Rails checks the lock_version. If another process has modified the record since it was loaded, Rails raises an ActiveRecord::StaleObjectError. You must handle this exception.

def purchase_product(product_id)
  product = Product.find(product_id)
  product.stock -= 1
  product.save!
rescue ActiveRecord::StaleObjectError
  puts "The product was updated by another process. Please retry."
end

How It Works

  1. When a record is loaded, Rails fetches its lock_version.
  2. When the record is updated, Rails checks if the lock_version in the database matches the one loaded.
  3. If the lock_version does not match, Rails raises an exception, preventing the update.

Example in Action

  1. User A loads the product (lock_version = 1).
  2. User B loads the product (lock_version = 1).
  3. User A updates the stock. The lock_version is incremented to 2.
  4. User B attempts to update the stock but receives a StaleObjectError because the lock_version is now 2, not 1.

Benefits of Optimistic Locking

  • Prevents accidental overwrites.
  • Easy to implement in Rails.
  • Works well when conflicts are rare but critical.

When to Use Optimistic Locking

Optimistic locking is ideal when updates to a record are infrequent but must be consistent. It works best for:

  • Inventory management.
  • User profile updates.
  • Financial transactions.

For scenarios with frequent updates, consider pessimistic locking, but be aware of potential deadlocks.

Optimistic locking is a simple yet powerful tool to maintain data integrity in multi-user environments. Implementing it ensures your application avoids race conditions and keeps data reliable.