The canonical example for an ActiveRecord callback is sending a welcome email after a
User is created.
class User < ActiveRecord::Base after_create :send_welcome_email private def send_welcome_email UserMailer.welcome_email.deliver end end
I remember how awesome that felt the first time. It seemed like such great design. I was fat-modeling, skinny-controllering with the best of them. But the joy didn’t last long.
One time, I manually created a bunch of users from the console in production. The intention was to set up their accounts and then personally send them an email inviting them to try out the product. But before I could do that I started getting confused emails asking why they’d been signed up for some product they’d never heard of. It hadn’t occurred to me that the welcome emails would be sent if I created users from the console.
Another time, I was trying to speed up an agonizingly slow test suite. Out of curiosity I commented out the welcome email callback on
User. The test suite ran 10 seconds or so faster. It hadn’t occurred to me that every time a user was created in a test it would send a mailer to the test delivery queue, and that all that time would add up to a significant amount.
I don’t mean to specifically pick on sending emails in a callback, that’s just a really common example. It could be changing an attribute before saving or even creating an associated record. The point is that when you use an ActiveRecord callback you’re saying you always want it to run every time. But that’s not what I really wanted.
I didn’t want a welcome email to be sent if I created a user from the console. I didn’t want a welcome email to be sent every time a user was created in a test. I really only wanted a welcome email to be sent when a user signed up. Which means the right place to send the email was in the controller, where it was in the first place before I tried to get clever.
Almost every ActiveRecord callback I’ve ever written I eventually removed later on after I realized it was actually contextual—it had only seemed like it should always run. Now I just don’t use them at all any more.
Inspired by Gary Bernhardt’s gem Do Not Want I wrote a gem that codified my intent not to use them. It’s called Hold Please and it will raise an exception if you or anyone else tries to use an ActiveRecord callback. As you’d expect it will allow third-party gems to use them so they don’t break.
If you want to ensure you don’t inadvertently relapse and prevent anyone else on your project from doing the same, check out Hold Please.
Enjoy your saner future.