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:
- Very easy to test.
- Avoids issues relating to transactions.
- Separates concerns making it easier for future developers to avoid unwanted side-effects.
- 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:
- Never use ActiveRecord callbacks.
- 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.
-
No app left behind: Upgrade your application to Ruby 3.0 and s...
-
A look forward from 2020
-
Testing Rails applications on real mobile devices (both design...
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.