Today I’ve learned something interesting. The new Rails callbacks after_create_commit, after_update_commit and after_destroy_commit can behave in a way I didn’t expect.

The after_commit callback is a well-known part of the Ruby on Rails framework. It’s called after a record has been created, updated, or destroyed and after the corresponding database transaction has been commited. It’s the primary method to use if we want to trigger a background job associated with a record.

class User < ActiveRecord::Base
  after_commit :schedule_welcome_email, on: :create  

  def schedule_welcome_email
    WelcomeEmailJob.perform_later(id)
  end
end

The Ruby on Rails 5 came with some new after_*_commit callbacks. Before it looked like this:

after_commit :action1, on: :create
after_commit :action2, on: :update
after_commit :action3, on: :destroy

And now we can use:

after_create_commit  :action1
after_update_commit  :action2
after_destroy_commit :action3

The problem

Let’s say we want to trigger the broadcast method after a record has been created or destroyed. We can try using the new callbacks:

class Comment < ActiveRecord::Base
  after_create_commit  :broadcast
  after_destroy_commit :broadcast

  def broadcast
    BroadcastJob.perform_later(id)
  end
end

That looks good! The after_destroy_commit works as expected. However, for some reason, the first after_create_commit is never triggered. But why?

The reason

Let’s take a look at the source code of the after_create_commit method:

def after_create_commit(*args, &block)
  set_options_for_callbacks!(args, on: :create)
  set_callback(:commit, :after, *args, &block)
end

As you can see, these methods are effectively aliases for the old after_commit callback with the :on option specified. And subsequent after_commit declarations override former declarations for the same method. That can be pretty surprising!

The solution

To solve this issue, we can use the old callback. The :on option supports an array of multiple life cycle events, so the solution is simple and looks like this:

after_commit :broadcast, on: [:create, :destroy]