The Power and Issues of Freezing Objects ...

The Power and Issues of Freezing Objects in Rails

Jul 21, 2024

image
What is frozen attributes

Frozen means 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 and Safety

    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: "[email protected]")
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 other ways to solve the above issue

There are alternative solutions available!

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



* Where there is Will there is Way *

Enjoy this post?

Buy Krishnabalaji R a coffee

More from Krishnabalaji R