Model callbacks in Phoenix, Ecto and Rails

SeriesPhoenix on Rails clock5 min read

Can you use model callbacks (after_create et al) in Phoenix just like in Rails? And more importantly, should you?

When I started working with Phoenix and Ecto, both at version 1.1, I've stumbled upon mentions of model callbacks both in the documentation (like here) and tutorials (like this one). Documentation looked suspicious though as callbacks were mentioned here and there but I couldn't find any solid info on them in main doc pages or guides. So I've decided to investigate.

Why bother

Why? Certainly not because I was missing them when I came from Rails. On the contrary, I expected Phoenix, the framework that has such great potential for dealing with bad parts of Rails, to get rid of model callbacks with iron fist. Here's a short list why:

  • callbacks encourage people to put more and more business logic into models
  • behavior for when the callback code fails is unclear and hard to control
  • as project grows, increasing number of callbacks execute in unclear order
  • callbacks make code composition harder (isn't that the whole beauty of Elixir?)

So, if I'm not after callbacks, why do I bother? Because by making them available and by putting emphasis on how cool they are, Phoenix/Ecto developers would encourage those new to the framework to start using one of the worst patterns that I had to deal with in Rails for years. Then, the pattern would sink into minds of programmers and before long, it would be too late. We'd see it used by fellow programmers in large teams and by authors of packages in hex.pm, effectively ruining our projects and the whole ecosystem (as explained in this huge article).

I could already see there's no strong emphasis put on them in Ecto 1.1 documentation, so thankfully the latter isn't true. But what about the former - are they available or not?

Ecto v2 to the rescue

OK, so it took a little digging but I've found the following crucial materials:

  1. Ecto v1.1 released and Ecto v2.0 plans This article states that callbacks are indeed there in Ecto v1, but will be deprecated in v1.1 and removed in v2. It also explains in detail why this decision was made. Examples are shown to present some of the aforementioned issues with callbacks. I really recommend reading it, especially to die-hard fans of Rails ways.
  2.  Ecto and preloading most recent record from association This thread from the Elixir Forum includes a bit of discussion about different views on ORMs. And about the responsibility that developers of frameworks have when it comes to introducing best practices and reiterating if those practices don't work.

So, Ecto did turn into callbacks at first, probably to offer an experience familiar to the one in Rails, but now it says farewell to them. And that's amazing!

It's a bit similar case to I18n (as mentioned in my other article): what started in Phoenix as a plain copy of Rails approach, became an idea of its own and a chance was taken to start clean with something that gives a better promise.

The Elixir Way

In fact, it's even better than if the approach present in v2 would be there from day one. Why? Because we can see that Phoenix developers are capable of taking a step back in order to make the ecosystem better. And that making Rails developers feel right at home is not their only priority. For your own good, they'll turn your cozy Rails habits upside down, if you didn't already do it yourself eg. by replacing callbacks with service object pattern or another.

José Valim has expressed all that perfectly in that Elixir Forum thread. I don't want to see his statement buried and forgotten there, so here it is:

We do have a responsibility though to talk about the cases where it does not work and I have failed to do so in the past. 

There's only one thing I can say to that: kudos José!

There's a term "The Rails Way" that basically means staying close to Rails conventions. Rails have done a great job at showing everyone how they can benefit from convention-driven project development. But conventions are not everything. As programmers, we're obligated to take our own conscious choices about when to play nice with what was given to us and when to ditch that. Maybe that's what "The Elixir Way" is about? I sure hope so.

Life after callbacks

Where should our precious callback code go? Take a look at the service pattern. One thing that will definitely improve using it in Phoenix is Ecto.Multi, which is a proper syntax for packing multiple repo operations into single transaction. It's a big improvement over wrapping everything with ActiveRecord::Base.transaction, which we have to do in service objects very often. You can learn more about it in this Ecto issue and in the documentation.

This is how you may create a transaction for inserting both new user and relevant notification:

lang:elixir
defmodule RegistrationService do
  alias Ecto.Multi

  def call(params) do
    Multi.new
    |> Multi.insert(:user, User.changeset(%User{}, params))
    |> Multi.insert(:notification, Notification.new_user_changeset(params))
  end
end

This is better than wrapping with transactions because you can see exactly what goes into the transaction, as opposed to wrapping vast parts of code with transaction block only because you have to include two DB calls at opposite sides of the service code in it. Also, it nicely extends the existing Ecto approach of separating model from repo. Service above doesn't rely on repo at all!

With Elixir's functional approach you have everything you need to keep your business logic and actions organized into neat modules with nicely composable functions within them.

Summary

Soon, there will be no built-in pattern for putting tons of business logic directly into models in Phoenix. By removing callbacks and encouraging composable functions with data flowing through them, Ecto v2 should not only help writing better callback-free code, but it'll become a more fitting member of the Elixir/Phoenix family.