ActiveRecord Callbacks Considered Harmful

Placeholder Avatar
Leonard Garvey
March 12, 2013

It is a truth, universally acknowledged, that as a Rails application grows it becomes more difficult to reason about side-effects. This post concerns one of the most common causes of confusion that I see within all but the most simple Rails applications: ActiveRecord callbacks.

What is an ActiveRecord callback?

Callbacks provide a Rails developer “hooks into the life cycle of an Active Record object”. Using callbacks we could easily configure an email to be sent when a user is created:

```ruby class User < ActiveRecord::Base after_create :send_welcome_email

def send_welcome_email UserMailer.welcome_email(self).deliver end end ```

This seems pretty great, we can be fairly certain that users will receive a welcome email when their account is created.

What’s the problem?

Assume you’ve got that code above and your boss comes up to you and says: “We’ve acquired another company and we want you to import their users into our database. Our customer support team will contact the 50,000 new users and prompt them to activate their account”.

You get to work and very quickly come up with a rake task to import a CSV file. Because your User model is 500 lines long and you’re under time pressure to get this job finished you forget that User.create(params) also sends an email! When you run the import, your Rails application dutifully sends out 50,000 emails welcoming users to your application even though they’ve never heard of you before! A client-service disaster ensues.

This scenario encapsulates the problem of side-effects. The biggest issue of ActiveRecord callbacks is they encourage Rails developers to introduce undocumented and unexpected side-effects into the the lowest level of the application. Furthermore you’re mixing business logic (welcoming users), into the database modelling layer of your application violating the single-responsibility principle.

Finally, there’s a huge issue with after_create as a callback. If you happen to wrap your create within a transaction, the after_create callback will be executed before the transaction finishes. Consider the following:

```ruby class User < ActiveRecord::Base after_create :log_creation

def log_creation Rails.logger.debug “user created” Rails.logger.debug to_json.to_s end end

User.transaction do User.create sleep 10 raise ActiveRecord::Rollback end ```

This code is obviously contrived, but when it is executed the following output appears:

DEBUG -- : (0.2ms) BEGIN DEBUG -- : SQL (0.5ms) INSERT INTO "users".... DEBUG -- : user created DEBUG -- : {"id":4,"name":null,"email":null,"created_at":"2013-11-29T02:32:56.529Z","updated_at":"2013-11-29T02:32:56.529Z"} DEBUG -- : (0.4ms) ROLLBACK

We can clearly see that the after_create callback is executed before the record will actually be saved in the database.

For these reasons I consider ActiveRecord callbacks harmful. Luckily, there’s a solution and it’s very simple.

What’s the fix?

We use the term “side-effect” as if it’s an unwanted thing, when the opposite is true. The side-effect here of sending a welcome email is as important as actually creating the user record. We just need to make it more explicit. My recommendation, and the approach I’ve been taking with many apps lately, is to use a factory object to do this work for us:

ruby class UserFactory def signup_user(user_params) User.create!(user_params).tap do |user| UserMailer.welcome_email(user).deliver end end end

The responsibility of that factory is to encapsulate the creation of users, and the related “side-effects”. This strategy has the following benefits:

  1. Very easy to test.
  2. Avoids issues relating to transactions.
  3. Separates concerns making it easier for future developers to avoid unwanted side-effects.
  4. Adheres to the Single Responsibility Principle.

In conclusion

Don’t use ActiveRecord callbacks. Use a factory, or a Service Object to separate those “side-effects” out of your models. Go a step further and make 2 rules for your Rails applications:

  1. Never use ActiveRecord callbacks.
  2. Never directly create ActiveRecord objects from your controllers, always use factories.