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

JSON:API consumption in Rails

14
.
11
.
2023
Jan Matusz
Ruby on Rails
Backend
Tutorial

Marketing hacks #01: How to Track off-line conversions

14
.
11
.
2023
Marek Łukaszuk
Ruby on Rails
Business
Marketing

Common communication issues in project management

02
.
10
.
2024
Michał Krochecki
Project Management

Selected SXSW lectures takeaways

14
.
11
.
2023
Michał Piórkowski
Conferences
Frontend
Backend
Business

SXSW Summary

14
.
11
.
2023
Michał Piórkowski
Ruby on Rails
Conferences
Frontend
Backend
Business

How to get the most out of SXSW Interactive

02
.
10
.
2024
Michał Krochecki
Ruby on Rails
Conferences
Frontend
Backend
Business

Guide to recruitment at Visuality

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

TOP Ruby on Rails Developers

14
.
11
.
2023
Maciej Zdunek
Ruby on Rails
Visuality
Business

How to conquer Westworld?

14
.
11
.
2023
Maciej Zdunek
Business
Marketing

2018 Rewind by Visuality

02
.
10
.
2024
Michał Krochecki
HR
Visuality

Quality Assurance Testing

14
.
11
.
2023
Jarosław Kowalewski
Ruby on Rails
Backend

Why do we like to be together?

02
.
10
.
2024
Michał Krochecki
Visuality
HR

Wallboards - a great value for our teams and clients

02
.
10
.
2024
Michał Krochecki
Ruby on Rails
Design
Project Management
Backend

2018 Clutch Global Leader

14
.
11
.
2023
Maciej Zdunek
Ruby on Rails
Visuality
Business
Marketing

Hot topic: Progressive Web Apps instead of native mobile apps

02
.
10
.
2024
Michał Krochecki
Ruby on Rails
Business
Backend
Frontend

Docker hosted on Jelastic

14
.
11
.
2023
Marcin Prokop
Ruby on Rails
Backend
Tutorial

All the pieces matter - Visuality DNA

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

Tech conferences 2018/2019 you definitely should attend

02
.
10
.
2024
Michał Krochecki
Conferences

Visuality Poznań is here!

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

Why we chose Ruby on Rails and React.js for our main technologies? (FAQ).

02
.
10
.
2024
Michał Krochecki
Ruby on Rails
Backend
Frontend
Visuality

Branding: How to style your Jira?

14
.
11
.
2023
Lukasz Jackiewicz
Tutorial
Design
Project Management

How to start your UX/UI designer career

14
.
11
.
2023
Bartłomiej Bednarski
Design
Tutorial
HR