Using custom ActiveRecord events/callbacks

Sometimes you are presented with a situation where you should use custom callbacks/events in your ActiveRecord model.

For example, consider the case where you need to ship the line items of an Order after it has been paid. Do you put the logic for creating a shipment in a controller, a model, or in an observer? If you use an observer, should you rely on the standard ActiveRecord callbacks like after_save?

Let's take a brief walk through each possibility in a progression that takes us from what may seem like the easiest solution to implement to what gives us the simplest, most maintainable code.

Using a controller to house this kind of logic violates SRP and leads to unnecessarily complex controller actions. As an added penalty it makes writing controller examples much more painful as there are more code execution paths. I don't think you should put your controller in the position of needing examples, but that's for another post.

There's got to be a better approach. Let's consider pushing this down into the Order model. This moves us in the spirit of skinny controller, fat model . After all an order knows when it has been paid:

While this has the benefit of keeping the controller simple it really only moves the problem. Now our model becomes more complex. It violates SRP by including functionality that it shouldn't be responsible for. Why should an order know when and how to create a shipment? Practically speaking this will make writing and maintaining the Order model and its examples more difficult.

I'd rather extract the behaviour out into an observer that can be responsible for listening to an event and then kicking off the action of creating the shipment. Doing this won't add unnecessary complexity to the Order model. Let's extract it out in an OrderObserver:

This isolates the responsibility in the OrderObserver. It also lets us write examples for this behaviour in isolation (Pat Maddox's no-peeping-toms plugin is a great tool for isolating observer examples from model examples).

It's reasonable to stop here and be satisfied, but the solution can still be improved with minimal effort. Rather than relying on the after_save callback in the OrderObserver it would be clearer to introduce a paid_in_full callback. This removes an unnecessary conditional as well as explicitly expresses the intent of the callback. This requires that we update both the OrderObserver and the Order model.

ActiveRecord already includes all of the necessary wiring to trigger events so we just need to update the Order model to fire the paid_in_full event at the right time:

Now when a payment is made an event will be fired and the OrderObserver will listen for it and start the process of creating an order shipment. This gives the code clean separation, meaningful names, and an ability to easily open and extend the system later should more functionality need to be added when an order is paid in full.

While it's not always necessary to create custom callbacks and events there are times when it adds value to the code by keeping the code clean, maintainable, and extendable. This is a technique I've been using for long time and I hope you might benefit from it as well.

Zach is a partner at Mutually Human and with over ten years of experience in professional software development. He believes strongly in taking a pragmatic approach to the testing and development process, and is a published contributor to The RSpec Book.