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_rollbackUpdating 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_rollbackDestroying an Object
ruby
1# Order of callbacks when destroying:
2before_destroy
3around_destroy
4after_destroy
5after_commit / after_rollbackDefining 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
22endBlock 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
9endLambda/Proc
ruby
1class User < ApplicationRecord
2 before_save ->(user) { user.email = user.email.downcase }
3endCommon 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
16endbefore_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
16endafter_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
19endafter_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
15endbefore_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
17endafter_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
9endConditional 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
17endHalting 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
12endCallback 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
31endTransaction 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
21endSkipping 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 timestampsBest 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
27endCommon 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)
19endCallbacks are powerful but use them wisely - complex callback chains can make debugging difficult!
