Understanding the Power and Problems of Freezing Objects in Rails

Understanding the Power and Problems of Freezing Objects in Rails


What is frozen attributes

Frozen is an object or attribute immutable, this means that once an object is frozen, you cannot modify it or its state.

When Rails needs to freeze an object, it typically does so by calling freeze on the object or its attributes at appropriate times in the object's lifecycle.

Immutability

Immutability refers to the concept that an object, once created, cannot be changed or modified it remains constant throughout its lifecycle.


Advantages of frozen attributes

  • Immutability
    Freezing attributes can prevent accidental or intentional modifications, which is particularly useful in situations where data integrity is critical.
  • Thread Safety
    Immutable objects are thread-safe since their state cannot be changed, reducing the chances of race conditions and making concurrent code easier to manage.
  • Performance Improvements
    Since frozen objects do not change, certain optimizations can be applied by the Ruby interpreter. For example, frozen strings can be reused across the application.

Disadvantages of frozen attributes

  • Complexity in Implementation
    Implementing immutability can add complexity to the codebase, especially when dealing with objects that have deeply nested structures or dependencies.
  • Increased Memory Usage
    In some cases, immutability might lead to increased memory usage due to the need to create new objects rather than modifying existing ones. This can be particularly relevant in applications with large data structures.
  • Difficulty in Debugging
    If not documented and managed properly, frozen attributes can lead to confusion and make debugging more challenging, as developers might not expect certain attributes to be immutable.

Is object can be freeze

Yes, in Ruby and Rails, you can freeze an entire object to make it immutable. When you freeze an object, you prevent any modifications to its state.

This can be useful in situations where you want to ensure that the object's state remains consistent and cannot be changed after it has been set.

user = User.new(name: "Rajesh", email: "rajesh.m@gmail.com")
user.freeze

# Attempting to modify the object will raise a RuntimeError
user.name = "Saji Sharma" # => RuntimeError: can't modify frozen User

Custom freezing logic

If you want to ensure that an object is frozen during or after certain operations, you need to implement this logic explicitly.

For instance, you can add a callback to freeze an object after it has been published:

class Product < ApplicationRecord
  after_save :freeze_name_if_published
 
  private

  def freeze_name_if_published
    name.freeze if published?
  end
end



Why did I start thinking about the frozen attribute, and what issue did I encounter?

Consider our Payment model, which includes multiple transactions. A payment is marked as "paid" after a successful transaction, updating the status column in the Payment model.

However, if a transaction is removed, the payment is reopened to be marked as "unpaid."

This scenario presents a significant issue.

class Payment < ApplicationRecord
  has_many :transactions, dependent: :destroy

end

class Transaction < ApplicationRecord
  belongs_to :payment
  after_save_commit :mark_payment_as_paid
  after_destroy_commit :reopen_the_payment

  private

  def mark_payment_as_paid
    payment.update(status: :paid) if is_successfull_transaction?
  end

  def reopen_the_payment
    payment.update(status: :open) # issue
  end
end

What is the issue? When we destroy the payment record, it also deletes the dependent transactions.

However, if the payment is reopened after a transaction, attempting to delete the payment will result in the following error.

FrozenError: can't modify frozen attributes

Rails will freeze the object, preventing further modifications until it is deleted.


How we solved the issue

We need to check if the object is frozen because we don't explicitly freeze it anywhere in the code. Therefore, we used this check to determine its state.

class Transaction < ApplicationRecord
  belongs_to :payment
  after_destroy_commit :reopen_the_payment

  private

  def reopen_the_payment
    return if payment.frozen?
    payment.update(status: :open) # issue
  end
end




What is the alternative solutions to solve the above issue

There are alternative solutions available!

Stay tuned, and we'll discuss them in detail later! Thank you!