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.