Rob Zolkos avatar

Rob Zolkos

February 10, 2020

Execution order of after_commit and after_rollback ActiveRecord callback

Rails allows the adding of various callbacks that trigger during the lifecycle of an ActiveRecord operation. The standard callbacks are before_validation, after_validation, before_save, before_create, after_create, after_save, after_commit, and after_rollback.

These are documented quite extensively in the Rails Guides and the Rails API documentation.

The documentation is quite good at showing you the order the callbacks are executed - eg before_validation fires before validate which fires before before_save etc. Most of these validations execute in the order they are defined. For example, if you have two before_save callbacks defined, then they will execute one after the other.

However there are two callbacks that do not execute in this order - after_commit and after_rollback. It is important to be aware of ActiveRecord execution order for these types of callback.

For example if you have three after_commit callbacks defined, how do you know which one is going to execute first? What if you have two other after_commit callbacks that are defined in concern modules and mixed in?

These two callbacks will fire in reverse order to how they are declared in your model.

Let’s go through a few scenarios with after_commit

A Single callback

  class Post
    after_commit :send_to_slack
  end

This is easy, it will get fired once, after the data is commited to the database.

Two callbacks

  class Post
    after_commit :send_to_slack
    after_commit :send_to_messenger
  end

The :send_to_messenger method will run before :send_to_slack

Three callbacks, one introduced from a concern

  # app/models/concerns/emailable.rb
  module Emailable
    extend ActiveSupport::Concern

    included do
      after_commit :send_to_email
    end
  end
  # app/models/post.rb
  class Post
    include Emailable

    after_commit :send_to_slack
    after_commit :send_to_messenger
  end

The :send_to_messenger method will run before :send_to_slack which will run before :send_to_email defined in the concern.

Four callbacks, two introduced from a concern

  # app/models/concerns/emailable.rb
  module Emailable
    extend ActiveSupport::Concern

    included do
      after_commit :send_to_email
      after_commit :send_to_admin_email
    end
  end
  # app/models/post.rb
  class Post
    include Emailable

    after_commit :send_to_slack
    after_commit :send_to_messenger
  end

The callbacks will run in this order :send_to_messenger, :send_to_slack, :send_to_admin_email, :send_to_email Notice how they are in reverse order to how they’ve been declared.

Five callbacks, three introduced from two concerns

  # app/models/concerns/emailable.rb
  module Emailable
    extend ActiveSupport::Concern

    included do
      after_commit :send_to_email
      after_commit :send_to_admin_email
    end
  end
  # app/models/concerns/carrier_pidgeonable.rb
  module CarrierPidgeonable
    extend ActiveSupport::Concern

    included do
      after_commit :send_by_carrier_pidgeon
    end
  end
  # app/models/post.rb
  class Post
    include Emailable
    include CarrierPidgeonable

    after_commit :send_to_slack
    after_commit :send_to_messenger
  end

Here we include a new concern - CarrierPidgeonable after Emailable. So the callback sequence changes to :send_to_messenger, :send_to_slack, :send_by_carrier_pidgeon, :send_to_admin_email, :send_to_email

Summary

Knowing the order of how your callbacks fire is important to debugging subtle bugs that can be caused by the misuse of callbacks. Whilst most callbacks fire in the order they are defined, after_commit and after_rollback callbacks fire in reverse order to how they are declared.