Optimistic Locking in Ruby on Rails: A Practical Guide
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
- When a record is loaded, Rails fetches its
lock_version
. - When the record is updated, Rails checks if the
lock_version
in the database matches the one loaded. - If the
lock_version
does not match, Rails raises an exception, preventing the update.
Example in Action
- User A loads the product (
lock_version = 1
). - User B loads the product (
lock_version = 1
). - User A updates the stock. The
lock_version
is incremented to2
. - User B attempts to update the stock but receives a
StaleObjectError
because thelock_version
is now2
, not1
.
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.