Being able to detect changes in model attributes is often essential for building reactive, state-aware systems in Rails. Whether you’re logging updates, triggering side effects, or enforcing validations, it helps to know what a value used to be before it was changed.
Rails makes this straightforward with the *_was method pattern—like.
In this post, we’ll take a closer look at how *_was
works in Active Record, how and when to use these methods effectively, and what to watch out for.
What is *_was in Active Record?
*_was
method is part of Active Record’s dirty tracking system. It returns the value of the given attribute before any in-memory changes were made. Essentially, this method allows you to retrieve the previous value of an attribute during the current lifecycle of the object—before it’s saved to the database.
Rails dynamically creates helper methods for each attribute in your model. These methods rely on the core method attribute_was
, which works like this:
1
2
3
def attribute_was(attr)
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
end
If the attribute was changed in memory, attribute_was
returns its previous value from the changed_attributes
hash. If it wasn’t changed, it simply returns the current value.
A Simple Example: Tracking Name Changes in Active Record
Let’s start with a simple example using a name
attribute.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Account < ApplicationRecord
# ...
end
account = Account.create(name: 'John')
# => account.name is now 'John'
account.name = 'Jane'
# => account.name is now 'Jane'
account.name_was
# => "John" (the value before the change)
account.save
# After saving, the dirty state is cleared.
account.name_was
# => "Jane" (if no further changes were made after saving)
- Before saving,
name_was
returns the original value ('John'
). - After saving, the previous value is cleared, so
name_was
returns the current value ('Jane'
), unless the attribute is changed again.
This is particularly useful in before_update
callbacks when you want to react to a change and compare old vs. new values.
A More Realistic Example: Responding to Status Transitions in Active Record
The power of *_was
methods really shines when you want to trigger logic based on specific state transitions in your models. Here’s an example from an e-commerce application where we want to react to order status changes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Order < ApplicationRecord
before_update :handle_status_transition
private
def handle_status_transition
return unless status_changed?
old_status = status_was
new_status = status
case [old_status, new_status]
when ['pending', 'shipped']
notify_shipped
when ['shipped', 'delivered']
notify_delivery
end
end
def notify_shipped
# Send shipping confirmation email
OrderMailer.shipping_confirmation(self).deliver_later
end
def notify_delivery
# Send delivery notification
OrderMailer.delivery_notification(self).deliver_later
end
end
status_changed?
ensures we only run the logic if the status actually changed.status_was
gives us the previous value before the change.status
gives us the new value after the change.- Based on specific transitions (e.g.
'pending'
→'shipped'
), we trigger different notifications.
Important Notes
While *_was
methods are convenient, there are a few things to keep in mind when using them:
1. Only Works Before Save
After saving, the dirty state is reset. That means status_was
no longer returns the pre-change value—it returns the current one.
1
2
3
4
5
user.status = 'active'
user.status_was # => 'inactive'
user.save
user.status_was # => 'active'
2. If the Attribute Didn’t Change, *_was == *_current
If no changes were made to the attribute, *_was
will be equal to the current value:
1
2
user = User.find(1)
user.status_was == user.status # => true
The *_was
value only differs from the current value if the attribute was changed in the current transaction (i.e., since the last save or load).
3. Doesn’t Persist History
These methods track in-memory changes only. If you need full audit logging or historical state tracking across multiple saves, consider using gems.
When to Use *_was Methods in Active Record
Use it in:
before_update
callbacks to compare old vs. new values- Conditional logic based on transitions (e.g. status, roles, flags)
- Logging or debugging attribute changes
- Sending notifications only on specific changes
Avoid using it for:
- After the record is saved (since the dirty state is cleared)
- Tracking changes across requests or sessions
- Storing a complete history of attribute changes (use something like PaperTrail for that)