ActiveRecord anti-patterns which you should avoid: Part 2. Callbacks

What are they?

ActiveRecord callback is a tricky Rails-way method to trigger some code in different phases of persisting model object.

Example:

class Order < ApplicationRecord
  before_save :set_total
  after_commit :send_order_confirmation, on: :create

# ...

  private

  def set_total
    self.total = order_items.sum{|item| item.quantity * item.unit_price}
    self.total += delivery_cost
  end

  def send_order_confirmation
    CustomerMailer.with(email: customer.email).order_confirmation.deliver_later
  end
end

It seems intuitive and simple at first. But when your model starts to grow, it gets unmaintainable. If there are more places in the code where your model is being saved, you start passing "if", "unless", "on" options to each callback registration. As with conditional validation, code is getting less and less maintainable.

What not to do instead?

Inlining your callbacks inside controller actions is not a way to go. They will quickly get overweighted. Controllers should take care of HTTP-related stuff, not sending emails.

What to do instead?

Inline them in service objects.

class PlaceOrderService
  def call(cart_uuid, place_order_command)
    place_order_command.validate!
    order = Order.find_by_cart_uuid(cart_uuid)
    order.assign_attributes(place_order_command.attributes)
    order.status = :placed

    order.total = order_items.sum{|item| item.quantity * item.unit_price}
    order.total += order.delivery_cost
    order.save(validate: false)
    CustomerMailer.with(email: order.customer.email).order_confirmation.deliver_later
  end
end

Pro-tip: Move side-effects to event handlers.

If you use a sort of event store in your app (i.e. Rails Event Store), consider moving side-effects (usually after-save/commit callbacks) to event handlers.