For the uninitiated, here’s how the rails website introduces and defines callbacks:
The following image shows which callbacks are available at different parts of an object’s life cycle and the order in which those callbacks are executed:
During the normal operation of a Rails application, objects may be created, updated, and destroyed. Active Record provides hooks (called callbacks) into this object life cycle so that you can control your application and its data. Callbacks allow you to trigger logic before or after an alteration of an object’s state.
While callbacks sound very handy, they can, and mostly do, become a pain very easily. This is mainly because callbacks change the linear flow of your code; making testing, debugging and refactoring a pain. Let’s say you run into a problem at the controller level and you want to dry run the code (or you just want to go through the code for any reason such as optimization), in theory this is what you will have to do:
-
- Start from controller method that caused the error
- Move to the model inspecting each method you have called in your controller
- end with the last line controller method.
Now if you are using callbacks, you will additionally have to check
-
- where the state of an object is being changed (i.e. whether it is being initialized, created, updated or deleted at any point)
- Then check for relevant callbacks that will be triggered for each state change including figuring out the order in which the callbacks will be executed as different order may affect state of the object in a different way.
As tiring as it sounds while reading here, it’s much worse when you actually have to do it. This debugging and maintenance nightmare is the reason we have a policy of avoiding callbacks as much as possible. However, we often end up getting stuck with them when we have to work on apps they we didn’t originally develop ourselves. For those daunting moments, I have a practice to create a callback card, as part of the technical documentation, for each object state which looks like this:
This card helps us keep track of all the callbacks being used and acts as a checklist when refactoring them into service objects or POROs (Plain Old Ruby Objects). For those rare occasions when we can’t escaping using a callback, we use the following best practices (described by Kelly Sutton in this article):
- Asynchronous by default: dont keep the user waiting while your callbacks do their job — unless the callback needs to do something as part of the transaction.
- Prefer
after_commit
toafter_save
: ensure transaction is actually committed before you go ahead and pass its information to another model, for example, for things like sending welcome email to a new user — unless you want to track changes in one of the objects attributes, for example, with method like user.phone_changed?. The *_changed? methods are not available in after_commit. More details about this can be found here.
The full article about callback best practices is available at the following link:
5 Rails Callbacks Best Practices Used at Gusto
Folks interviewing at Gusto are often surprised to discover that Gusto chooses the Ruby on Rails framework to write financial software.
You might also want to read the following articles about callbacks:
The biggest Rails code smell you should avoid to keep your app healthy
medium.com
The only acceptable use for callbacks in Rails ever
About three years ago, I worked at a product company where the central functionality in our app consisted of five or...
If you are Rails developer and struggle with callbacks as well, please do share how you usually deal with them. I wrote this article while creating a series of articles about Best practices for Ruby on Rails. Watch this space for details.