DEV Community

Yashika Vijayvargiya
Yashika Vijayvargiya

Posted on Originally published at Medium on

When to Choose Optimistic vs. Pessimistic Locking in Ruby on Rails

Not every concurrent database update needs the same locking strategy.

Imagine two users trying to book the last available seat on a flight. Both see that the seat is available and click “Book” almost simultaneously. Without proper concurrency control, both requests might attempt to reserve the same seat.

Ruby on Rails provides two common locking strategies to handle these situations: optimistic locking and pessimistic locking.

But the important question isn’t just how they work. It’s when should you choose one over the other?

Let’s explore the differences through practical Rails examples.

1. When Should You Choose Optimistic Locking?

Choose optimistic locking when conflicts are relatively rare and you want to detect conflicting updates without locking a database record in advance.

Rails supports optimistic locking using a lock_version column.

Real-world example: Editing a product

Imagine two administrators editing the same product in an e-commerce application.

  • Admin A opens the product and sees its price as ₹500.
  • Admin B opens the same product and sees its price as ₹500.
  • Admin A changes the price to ₹550 and saves.
  • Admin B changes the price to ₹600 and attempts to save.

Without concurrency control, Admin B could overwrite Admin A’s update without realizing it.

With optimistic locking, Rails detects that the record has changed since Admin B loaded it and raises ActiveRecord::StaleObjectError.

Implementation in Rails

First, add a lock_version column:

class AddLockVersionToProducts < ActiveRecord::Migration[7.2]
  def change
    add_column :products, :lock_version,
               :integer, default: 0, null: false
  end
end
Enter fullscreen mode Exit fullscreen mode

Rails automatically uses this column for optimistic locking.

product_a = Product.find(1)
product_b = Product.find(1)

product_a.update!(price: 550)
product_b.update!(price: 600)
# Raises ActiveRecord::StaleObjectError
Enter fullscreen mode Exit fullscreen mode

The second update fails because the record’s version has changed.

Choose optimistic locking when:

  • Multiple users may edit the same record, but simultaneous updates are uncommon.
  • Users spend time editing forms before saving.
  • You want to detect stale updates instead of blocking other transactions.
  • Your application can ask users to reload or resolve conflicting changes.

Typical use cases: Product descriptions, user profiles, document editing, and administrative settings.

2. When Should You Choose Pessimistic Locking?

Choose pessimistic locking when concurrent operations are likely and conflicting updates must be serialized.

Instead of allowing multiple transactions to modify the same record and detecting conflicts afterward, pessimistic locking acquires a database lock before performing the critical operation.

Rails provides pessimistic locking through methods such as with_lock and lock.

Real-world example: Booking the last available seat

Imagine a flight has only one seat remaining. Two customers try to book it at nearly the same time.

The application must ensure that only one customer can successfully reserve that seat.

FlightSeat.transaction do
  seat = FlightSeat.lock.find(seat_id)
raise "Seat already booked" if seat.booked?
  seat.update!(booked: true)
end
Enter fullscreen mode Exit fullscreen mode

The lock method issues a locking read, typically using SELECT ... FOR UPDATE on PostgreSQL. If another transaction already holds a conflicting row lock, the second transaction waits until that lock is released.

Once the first transaction commits, the second transaction can proceed and check the latest committed state. It then sees that the seat is booked and cannot reserve it again.

The lock is released when the transaction ends.

Choose pessimistic locking when:

  • Concurrent updates to the same record are likely.
  • An operation involves limited resources, such as seats, inventory, or account balances.
  • Conflicting operations must be serialized to maintain consistency.
  • Retrying failed operations would be expensive or difficult.

Typical use cases: Inventory deductions, seat reservations, financial balance updates, and processing shared resources.

3. Can You Use Both Locking Strategies in One Application?

Yes. Different operations can require different strategies.

For example, in an e-commerce application:

  • Editing product descriptions: Optimistic locking, because concurrent edits are relatively uncommon.
  • Reducing stock during checkout: Pessimistic locking when you need to serialize updates to a specific inventory row.
  • Updating account settings: Optimistic locking, especially when users edit forms over a longer period.
  • Allocating a limited resource: Pessimistic locking when competing transactions must check and claim the same resource safely.

Choose the strategy based on the operation’s concurrency characteristics and business requirements, rather than applying one locking strategy to every model.

4. What Should You Consider Before Choosing?

Before implementing either strategy, ask yourself these questions:

  1. How frequently can conflicts occur? If simultaneous updates are rare, optimistic locking may be sufficient.
  2. What happens if an update fails? If a user can safely reload and retry, optimistic locking is often appropriate.
  3. Must the operation be serialized? If competing transactions must check and claim a resource in sequence, pessimistic locking may be a better fit.
  4. How long will the transaction run? Keep pessimistic locking transactions short. Never hold a database lock while waiting for user input or making slow external HTTP calls.
  5. What other safeguards are needed? Database constraints, atomic updates, and appropriate isolation levels can be essential regardless of the locking strategy.

Conclusion

The choice between optimistic and pessimistic locking depends on how your application handles concurrency.

Use optimistic locking when conflicts are uncommon and you can detect and resolve stale updates. Use pessimistic locking when concurrent operations must be serialized to protect shared resources.

The goal isn’t to lock everything. It’s to select the simplest concurrency-control strategy that preserves your application’s data integrity without unnecessarily reducing performance.

Top comments (0)