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 six domain models with excessive callbacks. I often found myself attacking the knotty nastiness for days at a time, trying to track down stubborn bugs.
Some time later, I stood up in front of the crowd at the Atlanta Ruby User’s Group and said, “I hate callbacks! What good are they?”
Today, I have the answer.
Let’s try an example
To demonstrate where callbacks are acceptable, let’s use the canonical Rails example, an online store.
As usual, there are Products, Orders, and LineItems, but we’ll be focusing on Orders. An Order instance has many LineItem instances. For optimization purposes, an Order keeps a copy of the total price, which is the sum of all line item prices.
On our sign-up page, we have a check box where a customer can indicate that they’re a returning customer. Returning customers get 10% off of the total price! Order#returning_customer will represent this information in the database.
So far, we’ve detailed two possible candidates for callbacks necessary to maintain the correct state of an Order:
- Order#total_price (based on the sum of line items)
- Order#total_price (to be updated based on whether we have a returning customer)
A naive approach would be to add callback logic to the Order model to sum all associated line items and apply the returning customer discount. However, adding a callback for #1 would violate the Single Responsibility Principle, as an Order would now be required to know about attributes of line items and how to sum them.
Here’s the correct approach.
The simple rule
An Order may have a callback for #2 because there are no external dependencies in the callback logic! That’s it. That’s the simple rule. Use a callback only when the logic refers to state internal to the object.
You may now be asking, but how does an Order then determine the total_price? That’s the responsibility of a higher-level service object that manages the interaction between Orders and LineItems, perhaps a controller action or, preferably, a plain old Ruby object.
When do you use callbacks?