DEV Community

Cover image for Parsimony with ActiveRecord callbacks
Lucas Sapienza
Lucas Sapienza

Posted on • Originally published at sapienza.tech

Parsimony with ActiveRecord callbacks

It's not rare to see someone associating activerecord callbacks with something bad or wrong, avoiding at all cost using them and thinking in other ways to achieve the desired goal, although i believe that nothing is absolute and there are situations where they are needed.

Taking the following scenario as example

class Purchase < ApplicationRecord
  belongs_to :user

  def confirm!(user)
    self.user = user
    self.confirmed = true
    PurchaseProcessor.create!(user: user)
    Save!
  end
end

We have the class Purchase which have a methodconfirm!Responsible for changing the state ofPurchase, however this can lead us to a problem of consistency capable of creating a headache because the ActiveRecord automatically generates methods for the attributes and they can be changed in other fluxes

purchase.update_attributes!(confirmed: true)
Purchase.new(confirmed: true).save
Purchase.create!(confirmed: true)

This is a big problem having in mind that the state of the object is inconsistent, it is confirmed but dont have user and the processor wasnt executed

If exist a worker that generates any important report of purchases with base at a condition setted on PurchaseProcessor, the chance to leave your base inconsistency is huge

One option would be using callbacks

class Purchase < ApplicationRecord
  belongs_to :user

  validates_presence_of :user_id, if: :confirmed?

  after_save :execute_purchase_processor_on_confirm

  private

  def execute_purchase_processor_on_confirm
    If confirmed? && confirmed_changed?
      PurchaseProcessor.create!(user: user)
    end
  end
end

In that way, independently of how any attribute is setted in any flux we have the garantee of the integrity

But there are cases where callbacks became a problem by itself, eg:

class User < ApplicationRecord
  after_create :send_confirmation_email

  def send_confirmation_email
    UserMailer.registration_confirmation(self).deliver
  end
end

This way maybe its necessary to create an user anywhere without calling the send_confirmation_email, and with this design you can just do it by using the private method create_without_callbacks who also ignores the validations ( not a good option )

I think it worths the question, how frequently you will have to create an user withotu sending the confirmation email? maybe it deserves to be splited in other class, eg:

class UserRegistration
  def initialize(user)
    @user = user
  end

  def register
    @user.save
    UserMailer.registration_confirmation(self).deliver
  end
end

Maybe its better to split behaviors that is not expected by default with the goal to simplify the use and manteining ( you expect that creating a User just create it without executing other actions or fluxes )

Callbacks are not all evil, but i believe that should had some reflection before using it, thinking about the context and the scenario... but as as rule i particularly like to start thinking about the design without the use of the callbacks and analyse if it can cause some inconsistency or indesired scenario

Top comments (1)

Collapse
 
janko profile image
Janko Marohnić

In the callback example, the downside is that now in tests you're not able to update the confirmed column without triggering the purchase processor.