What Goes In Active Records. In October 2011, Gary Bernhardt did a… | by clay schöntrup | Medium
- Select a language for the TTS:
- UK English Female
- UK English Male
- US English Female
- US English Male
- Australian Female
- Australian Male
- Language selected: (auto detect) - EN
Play all audios:

In October 2011, Gary Bernhardt did a two-part screencast titled “What Goes In Active Records” (1, 2). Starting with general principles, Gary states his goals for an ActiveRecord class’s
design. > The things I want in this class are things that wrap the database in > thin ways, and hide the database details from the outside. The main > thing I don’t want in this
class is anything that could be > considered application logic; which means probably anything with a > conditional in it is not going to belong in this class. DON’T USE CALLBACKS One
of his first specific critiques of his code are these two callbacks, which Gary says he added specifically to test his theory that callbacks were a bad idea.
before_create(:create_braintree_customer) before_save(:update_braintree_email) The first callback calls out to the Braintree API every time a User is created. This leads to numerous examples
of Gary having to stub that call, like this: before { fake_braintree! } In every Rails app I’ve ever worked that had callbacks (I’ve worked on something like 13 Rails apps at various
startups), it has lead to this type of strategy littered throughout the codebase. We’re essentially saying, “do this thing universally”, but then complicating the app by adding a bunch of
intervening code to say, “except no wait, don’t do it in these special cases.” It never turns out well. Another common callback strategy is to ensure that, say, every newly created Group
gets an admin User, or e.g. a job is enqueued. This creates numerous unnecessary database calls that can significantly impact the performance of a large test suite. (I’ve written before on
how a similar problem can result from validating association presence.) Regardless of whether we stub out these calls or let them call through, we’re incurring a cost, even when we’re just
creating records to test a scope for instance. A THORNIER PROBLEM So far I’ve discussed a _general_ problem with callbacks, concerning performance and code complexity. But Gary’s callback
example soon becomes more interesting when we think about callback execution order, which includes these. before_save -> before_create -> save This creates a “gotcha” when creating a
record for the first time. The before_save callback attempts to update a Braintree customer who has not been created yet. This leads Gary to wrap his callback in a guard clause. def
update_braintree_email return unless persisted? && email_changed? # <= Complexity! Braintree::Customer.update!(braintree_customer_id, email: email) end THE SOLUTION These
create and update cases originate with the UsersController, so Gary simply extracts these behaviors into collaborator classes like CreateUser and UpdateUserEmail which can be called from
UsersController#create and UsersController#update respectively. The collaborator simply creates the user in Braintree, and _then_ creates the database record. The callbacks go away as does
the need for complicated hacks like the fake_braintree! method. The key insight is that this approach removes an unnecessary conditional. When it comes to instance methods, like
next_billing_date, we can simply extract them to a presenter class using a SimpleDelegator like so. class UserBillingPresenter < SimpleDelegator def next_billing_date # Some long
method with business logic. end end@user_presenter = UserBillingPresenter.new(user) # controller<%= @user_presenter.next_billing_date %> # view SUMMARY The pragmatist in us says
there are exceptions to every rule, and we shouldn’t be dogmatic. Are there caveats where using callbacks is worth the cost? Maybe, but I can’t say that I’ve encountered any that were
compelling to me. The more important issue to me is that if a rule helps 95% of the time, and hurts 5% of the time, it’s probably better to just generally adhere to it. Because in all but
the smallest teams, it can be much harder to enforce consistent standards for when a risky pattern is worth the cost—whereas a simple prohibition like “don’t use callbacks” is easy for
everyone to remember. If you’re lucky enough to have a small team of seniors, you may be fine. I’ve been burned too many times in the seven years since I first saw those Gary Bernhardt
videos in 2011.