Skip
Arish's avatar

24. Callbacks


Active Record Callbacks

Callbacks are methods that get called at certain points in an object's lifecycle. They allow you to trigger logic before or after changes to an object's state.

The Callback Lifecycle

Creating an Object

ruby
1# Order of callbacks when creating:
2before_validation
3after_validation
4before_save
5around_save
6before_create
7around_create
8after_create
9after_save
10after_commit / after_rollback

Updating an Object

ruby
1# Order of callbacks when updating:
2before_validation
3after_validation
4before_save
5around_save
6before_update
7around_update
8after_update
9after_save
10after_commit / after_rollback

Destroying an Object

ruby
1# Order of callbacks when destroying:
2before_destroy
3around_destroy
4after_destroy
5after_commit / after_rollback

Defining Callbacks

Method Reference

ruby
1class User < ApplicationRecord
2  before_save :normalize_email
3  after_create :send_welcome_email
4  before_destroy :check_if_can_destroy
5  
6  private
7  
8  def normalize_email
9    self.email = email.downcase.strip
10  end
11  
12  def send_welcome_email
13    UserMailer.welcome(self).deliver_later
14  end
15  
16  def check_if_can_destroy
17    if admin?
18      errors.add(:base, "Cannot delete admin users")
19      throw :abort
20    end
21  end
22end

Block Syntax

ruby
1class User < ApplicationRecord
2  before_save do
3    self.email = email.downcase if email.present?
4  end
5  
6  after_create do |user|
7    Rails.logger.info "Created user: #{user.email}"
8  end
9end

Lambda/Proc

ruby
1class User < ApplicationRecord
2  before_save ->(user) { user.email = user.email.downcase }
3end

Common Callbacks

before_validation

ruby
1class User < ApplicationRecord
2  before_validation :set_defaults
3  before_validation :normalize_data
4  
5  private
6  
7  def set_defaults
8    self.role ||= 'user'
9    self.status ||= 'pending'
10  end
11  
12  def normalize_data
13    self.email = email.downcase.strip if email.present?
14    self.phone = phone.gsub(/\D/, '') if phone.present?
15  end
16end

before_save

ruby
1class Article < ApplicationRecord
2  before_save :generate_slug
3  before_save :set_published_at
4  
5  private
6  
7  def generate_slug
8    self.slug = title.parameterize if title_changed?
9  end
10  
11  def set_published_at
12    if published? && published_at.nil?
13      self.published_at = Time.current
14    end
15  end
16end

after_create

ruby
1class User < ApplicationRecord
2  after_create :send_welcome_email
3  after_create :create_default_settings
4  after_create :notify_admin
5  
6  private
7  
8  def send_welcome_email
9    UserMailer.welcome(self).deliver_later
10  end
11  
12  def create_default_settings
13    settings.create!(theme: 'light', notifications: true)
14  end
15  
16  def notify_admin
17    AdminNotifier.new_user(self).deliver_later
18  end
19end

after_save

ruby
1class Product < ApplicationRecord
2  after_save :update_search_index
3  after_save :clear_cache
4  
5  private
6  
7  def update_search_index
8    SearchIndexer.perform_async(id)
9  end
10  
11  def clear_cache
12    Rails.cache.delete("product:#{id}")
13    Rails.cache.delete("products:all")
14  end
15end

before_destroy

ruby
1class User < ApplicationRecord
2  before_destroy :check_for_orders
3  before_destroy :archive_data
4  
5  private
6  
7  def check_for_orders
8    if orders.pending.any?
9      errors.add(:base, "Cannot delete user with pending orders")
10      throw :abort
11    end
12  end
13  
14  def archive_data
15    DataArchiver.archive_user(self)
16  end
17end

after_destroy

ruby
1class Attachment < ApplicationRecord
2  after_destroy :delete_file_from_storage
3  
4  private
5  
6  def delete_file_from_storage
7    FileStorage.delete(file_key)
8  end
9end

Conditional Callbacks

ruby
1class User < ApplicationRecord
2  after_save :notify_admin, if: :role_changed?
3  before_save :encrypt_password, if: :password_changed?
4  after_create :send_email, unless: :skip_email?
5  
6  # Multiple conditions
7  after_save :update_index, if: [:published?, :content_changed?]
8  
9  # Lambda condition
10  before_destroy :check_permission, if: -> { !Rails.env.test? }
11  
12  private
13  
14  def skip_email?
15    imported? || test_account?
16  end
17end

Halting Execution

Use throw :abort to stop the callback chain:

ruby
1class Order < ApplicationRecord
2  before_save :check_inventory
3  
4  private
5  
6  def check_inventory
7    if items.any? { |item| !item.in_stock? }
8      errors.add(:base, "Some items are out of stock")
9      throw :abort  # Prevents save
10    end
11  end
12end

Callback Classes

For complex or reusable callback logic:

ruby
1# app/models/concerns/user_callbacks.rb
2class UserCallbacks
3  def after_create(user)
4    send_welcome_email(user)
5    create_default_settings(user)
6  end
7  
8  def before_destroy(user)
9    archive_user_data(user)
10  end
11  
12  private
13  
14  def send_welcome_email(user)
15    UserMailer.welcome(user).deliver_later
16  end
17  
18  def create_default_settings(user)
19    user.settings.create!(defaults: true)
20  end
21  
22  def archive_user_data(user)
23    DataArchiver.archive(user)
24  end
25end
26
27# app/models/user.rb
28class User < ApplicationRecord
29  after_create UserCallbacks.new
30  before_destroy UserCallbacks.new
31end

Transaction Callbacks

These run after the database transaction commits or rolls back:

ruby
1class Order < ApplicationRecord
2  after_commit :send_confirmation_email, on: :create
3  after_commit :sync_with_external_system
4  after_rollback :handle_failed_save
5  
6  private
7  
8  def send_confirmation_email
9    # This runs after the transaction commits
10    # Safe to reference the order ID
11    OrderMailer.confirmation(self).deliver_later
12  end
13  
14  def sync_with_external_system
15    ExternalSyncJob.perform_later(id)
16  end
17  
18  def handle_failed_save
19    Rails.logger.error "Order save failed: #{errors.full_messages}"
20  end
21end

Skipping Callbacks

ruby
1# These methods skip callbacks:
2user.update_column(:name, "value")
3user.update_columns(name: "value", email: "email")
4User.update_all(status: "active")
5user.delete  # vs user.destroy
6User.delete_all  # vs User.destroy_all
7
8# Using touch: false
9article.save(touch: false)  # Skips touching timestamps

Best Practices

ruby
1class User < ApplicationRecord
2  # 1. Keep callbacks simple and focused
3  after_create :send_welcome_email
4  
5  # 2. Use background jobs for slow operations
6  after_commit :sync_to_crm, on: :create
7  
8  # 3. Avoid callbacks that modify other records
9  # Bad: after_save :update_all_related_records
10  
11  # 4. Use service objects for complex logic
12  after_create :setup_account
13  
14  private
15  
16  def send_welcome_email
17    UserMailer.welcome(self).deliver_later
18  end
19  
20  def sync_to_crm
21    CrmSyncJob.perform_later(id)
22  end
23  
24  def setup_account
25    AccountSetupService.new(self).call
26  end
27end

Common Pitfalls

ruby
1# 1. Infinite loops - callbacks triggering updates that trigger callbacks
2after_save :update_related
3def update_related
4  related_record.update(field: value)  # Might trigger callbacks
5end
6
7# Solution: Use update_column or check if change is needed
8def update_related
9  related_record.update_column(:field, value) if field_changed?
10end
11
12# 2. Slow callbacks blocking requests
13after_create :heavy_processing  # Blocks the request!
14
15# Solution: Use background jobs
16after_create :schedule_processing
17def schedule_processing
18  HeavyProcessingJob.perform_later(id)
19end

Callbacks are powerful but use them wisely - complex callback chains can make debugging difficult!