29
.
04
.
2024
3
.
10
.
2022
Ruby on Rails
Backend
Tutorial

Should I use Active Record Callbacks?

Mateusz Woźniczka
Ruby Developer

Callbacks are methods that are called at specified moments of an object's life. They can be called whenever the object is created, updated, saved, deleted, validated, or loaded from the database.

Let's assume that in our Blog app, we want to perform the following actions whenever a new post is added:

  • make sure that the title is uppercase
  • send an email notification to subscribers

Our code (utilizing callbacks) might look as follows:

# models/post.rb
class Post < ApplicationRecord
  before_save :format_title
  after_create :send_email_notification

  private

  def format_title
    title.upcase!
  end

  def send_email_notification
    puts "SENDING EMAIL - Post #{id}"
    UserMailer.with(post_id: id).new_post_added.deliver_later
  end
end

Everything seems to be working as intended: the title of the post is capitalized, and an email has been sent.

> Post.create(title: 'a title', body: 'a body')
   (0.6ms)  SELECT sqlite_version(*)
  TRANSACTION (0.1ms)  begin transaction
  Post Create (0.4ms)  INSERT INTO "posts" ("title", "body", "created_at", "updated_at") VALUES (?, ?, ?, ?)  
    [["title", "A TITLE"], ["body", "a body"], ["created_at", "2022-08-18 22:45:27.863105"], ["updated_at", "2022-08-18 22:45:27.863105"]]
SENDING EMAIL - Post 5
  TRANSACTION (2.0ms)  commit transaction
=> <Post id: 5, title: "A TITLE", body: "a body", created_at: "2022-08-18 22:45:27.863105000 +0000", 
        updated_at: "2022-08-18 22:45:27.863105000 +0000">

It looks like Rails allows us to add new functionality linked to the object's lifecycle in a really simple, quick, and DRY way.

Sharp knives

One of the Rails doctrines is to Provide sharp knives. It means that Rails supplies some features, that can cause a lot of harm if used improperly or without full understanding.

Active Record Callback is an example of such a feature because using it often ends up making the code harder to read and maintain. The following examples describe some of the potential issues, that using callbacks could cause.

Callbacks run every time

Every time an object's state changes all callbacks linked to that change are called. It means, that:

  • format_title method is invoked whenever a post record is about to be saved. This might be helpful to keep the format of the titles consistent, and not cause any real trouble.
  • every time a new post is created an email notification is sent. This can cause some issues because we might not want to send an email when the post is created during debugging, testing, or creating posts in some other way (such as bulk import or any other functionality, that might be implemented in the future).

Even if a callback is fitting perfectly our current needs, it is very probable, that we will have to change it or even get rid of it during the implementation of a new feature. And this means additional work - sometimes quite a lot of it.

Testing models with callbacks is harder

As we already know, callbacks run every time, so in our case, this means, that an email notification will be sent every time a Post object is saved in the DB. This is obviously something we want to avoid, and there are plenty of ways to do it (configuring dev and test mailers, so emails are not delivered, or using some gems). But it won't work in cases when a callback is making some API call, or calling some other service. In this case, we may need to use stubs or mocks as a workaround - yet again more work to do.

Callback may increase coupling

Callbacks (especially the ‘after’ ones) are somehow encouraging to put some of the domain logic into them because it makes the feature implementation faster.

In our example, we want to send an email notification, every time a new post is added. Triggering email delivery from after create callback is very easy to implement, but it increases the coupling between Post and UserMailer classes. Every change we make in UserMailer class can potentially cause a change in behavior of Post object which is an obvious antipattern.

It is hard to pick the right callback for the job

There are about 13 different callbacks, and some of them have very subtle differences. Picking the one, that we actually need may be quite difficult

For example, if we change after_create to after_commit we end up with the following code:

# models/post.rb
class Post < ApplicationRecord
  before_save :format_title
  after_commit :send_email_notification

  private

  def format_title
    title.upcase!
  end

  def send_email_notification
    puts "SENDING EMAIL - Post #{id}"
    UserMailer.with(post_id: id).new_post_added.deliver_later
  end
end

In this case, an email is sent after the DB transaction.

> Post.create(title: 'a title', body: 'a body')
   (0.7ms)  SELECT sqlite_version(*)
  TRANSACTION (0.1ms)  begin transaction
  Post Create (0.4ms)  INSERT INTO "posts" ("title", "body", "created_at", "updated_at") VALUES (?, ?, ?, ?)  
    [["title", "A TITLE"], ["body", "a body"], ["created_at", "2022-08-18 23:38:24.943231"], ["updated_at", "2022-08-18 23:38:24.943231"]]
  TRANSACTION (3.2ms)  commit transaction
SENDING EMAIL - Post 6
=> <Post id: 6, title: "A TITLE", body: "a body", created_at: "2022-08-18 23:38:24.943231000 +0000", 
        updated_at: "2022-08-18 23:38:24.943231000 +0000">

The difference seems to be very subtle. But if a transaction fails, data added by it will be rolled back. It means that after_commit won't send the email, but after_save will send it (so we end up with an email sent, that informs about the new post, which has not been added).

The execution order of callbacks is tricky

Callbacks are triggered by specific moments in the object's lifecycle. It is obvious, that before_save will be called before after_save. When we have multiple callbacks of the same type, they will be called in the order in which they are defined.

Things become more tricky, when we have multiple callbacks, that are triggered by the same event - then the more generic one (after_save) runs after the more specific ones (after_create).

Let's consider the following class:

# models/post.rb
class Post < ApplicationRecord
  after_save :send_email_to_author
  after_create :send_twitter_notification
  after_create :send_email_notification

  private

  def send_email_notification
    puts "SEND EMAIL - Post #{id}"
    UserMailer.with(post_id: id).new_post_added.deliver_later
  end

  def send_email_to_author
    puts 'SEND EMAIL TO AUTHOR'
    UserMailer.with(post_id: id,
                    author_id: author.id).publication_notification.deliver_later
  end

  def send_twitter_notification
    puts 'SEND TWITTER NOTIFICATION'
      TwitterPublisher.perform_later(post_id: id)
  end
end

The creation of a new Post object looks in this case as follows:

> > Post.create(title: 'A TITLE', body: 'a body')
   (0.8ms)  SELECT sqlite_version(*)
  TRANSACTION (0.1ms)  begin transaction
  Post Create (1.8ms)  INSERT INTO "posts" ("title", "body", "created_at", "updated_at") VALUES (?, ?, ?, ?)  
    [["title", "A TITLE"], ["body", "a body"], ["created_at", "2022-08-19 23:15:10.322892"], ["updated_at", "2022-08-19 23:15:10.322892"]]
SEND TWITTER NOTIFICATION
SENDING EMAIL - Post 8
SEND EMAIL TO AUTHOR
  TRANSACTION (0.6ms)  commit transaction
=> <Post id: 8, title: "A TITLE", body: "a body", created_at: "2022-08-19 23:15:10.322892000 +0000", 
        updated_at: "2022-08-19 23:15:10.322892000 +0000">

As we can see, the callbacks are run in the following order: send_twitter_notification, send_email_notification, send_email_to_author which may be a bit counterintuitive.

Summary

Active Record Callbacks at first seem to be yet another nice Rails feature helping us write the code the 'Rails way'. But actually, when the application grows the profits of using them are quickly buried by lots of additional work, that is needed to work around them.

If you really have to use a callback, make sure, that it interferes only with the object, that triggered callback in the first place.

Mateusz Woźniczka
Ruby Developer

Check my Twitter

Check my Linkedin

Did you like it? 

Sign up To VIsuality newsletter

READ ALSO

Safe navigation operator '&.' vs '.try' in Rails

14
.
11
.
2023
Mateusz Woźniczka
Ruby on Rails
Backend
Ruby
Tutorial

What does the ||= operator actually mean in Ruby?

14
.
11
.
2023
Mateusz Woźniczka
Ruby on Rails
Backend
Ruby
Tutorial

How to design an entity - DDD in Ruby on Rails

17
.
03
.
2024
Paweł Strzałkowski
Ruby on Rails
Domain-Driven Design
Backend
Tutorial

Entity - DDD in Ruby on Rails

17
.
03
.
2024
Paweł Strzałkowski
Ruby on Rails
Domain-Driven Design
Backend
Tutorial

Should I use instance variables in Rails views?

14
.
11
.
2023
Mateusz Woźniczka
Ruby on Rails
Frontend
Backend
Tutorial

Data Quality in Ruby on Rails

14
.
11
.
2023
Michał Łęcicki
Ruby on Rails
Backend
Software

We started using Event Storming. Here’s why!

14
.
11
.
2023
Mariusz Kozieł
Event Storming
Visuality

First Miłośnicy Ruby Warsaw Meetup

14
.
11
.
2023
Michał Łęcicki
Conferences
Visuality

Should I use Action Filters?

14
.
11
.
2023
Mateusz Woźniczka
Ruby on Rails
Backend
Tutorial

Value Object - DDD in Ruby on Rails

17
.
03
.
2024
Paweł Strzałkowski
Ruby on Rails
Domain-Driven Design
Backend
Tutorial

Introduction to DDD in Ruby on Rails

17
.
03
.
2024
Paweł Strzałkowski
Ruby on Rails
Domain-Driven Design
Backend
Tutorial

Safe data migrations in Rails

17
.
03
.
2024
Paweł Strzałkowski
Ruby on Rails
Backend
Tutorial

I love dev, and so do we!

14
.
11
.
2023
Michał Łęcicki
Software
Conferences

Updated guide to recruitment process at Visuality

14
.
11
.
2023
Michał Łęcicki
Visuality
HR

Visuality Academy for wannabe Junior Engineers

14
.
11
.
2023
Michał Piórkowski
HR
Visuality

How to approach funding as an MVP

14
.
11
.
2023
Michał Piórkowski
Business
Startups

Visuality 13th birthday

14
.
11
.
2023
Michał Piórkowski
HR
Visuality

How To Receive Emails With a Rails App in 2021

14
.
11
.
2023
Michał Łęcicki
Ruby on Rails
Tutorial

Project Quality in IT - How to Make Sure You Will Get What You Want?

02
.
10
.
2024
Wiktor De Witte
Ruby on Rails
Project Management
Business

5 Trends in HR Tech For 2021

14
.
11
.
2023
Maciej Zdunek
Business
Project Management