Skip to content
PLAY VIDEO PLAY VIDEO PLAY VIDEO
By Leonard Garvey

ActiveRecord callbacks considered harmful

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:

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:

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:

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.

Latest Articles by Our Team

Our expert team of designers and developers love what the do and enjoy sharing their knowledge with the world.

We Hire Only the Best

reinteractive is Australia’s largest dedicated Ruby on Rails development company. We don’t cut corners and we know what we are doing.

We are an organisation made up of amazing individuals and we take pride in our team. We are 100% remote work enabling us to choose the best talent no matter which part of the country they live in. reinteractive is dedicated to making it a great place for any developer to work.

Free Community Workshops

We created the Ruby on Rails InstallFest and Ruby on Rails Development Hub to help introduce new people to software development and to help existing developers hone their skills. These workshops provide invaluable mentorship to train developers, addressing key skills shortages in the industry. Software development is a great career choice for all ages and these events help you get started and skilled up.

  • Webinars

    Webinars

    Webinars are our online portal for tips, tricks and lessons learned in everything we do. Make the most of this free resource to help you become a better developer.

    Learn more about webinars

  • Installfest

    Installfest

    The Ruby on Rails Installfest includes a full setup of your development environment and step-by-step instructions on how to build your first app hosted on Heroku. Over 1,800 attendees to date and counting.

    Learn more about Installfest

  • Development Hub

    Development Hub

    The Ruby on Rails Development Hub is a monthly event where you will get the chance to spend time with our team and others in the community to improve and hone your Ruby on Rails skills.

    Learn more about Development Hub

Get the “reinteractive Review” Monthly Email