Introduction
Transaction callbacks run after the database transaction is committed or rolled back. They are crucial for side effects that should only happen if the save actually persisted to the database. If you have ever sent an email for a record that was rolled back, you understand why these callbacks exist.
Key Concepts
after_commit: Runs only after the wrapping database transaction has been successfully committed.after_rollback: Runs only after the wrapping transaction has been rolled back.after_create_commit/after_update_commit/after_destroy_commit: Shorthand forafter_commitscoped to a specific action.after_save_commit: Runs after commit for both create and update actions.
Real World Context
In production, database operations often happen inside transactions. A checkout process might create an order, update inventory, and charge a payment. If the payment fails, the transaction rolls back. If you sent a confirmation email in after_save, the user receives a confirmation for an order that does not exist. after_commit prevents this class of bugs.
Deep Dive
The Problem with after_save
rubyclass Order < ApplicationRecord after_save :send_confirmation_email # DANGER! private def send_confirmation_email OrderMailer.confirmation(self).deliver_later end end # Problem scenario: Order.transaction do order = Order.create!(...) # Email enqueued! raise "Something went wrong" # Transaction rolls back # But email was already enqueued for a non-existent order! end
The email is enqueued during after_save, which runs inside the transaction. When the transaction rolls back, the order disappears, but the email job is already in the queue.
after_commit to the Rescue
rubyclass Order < ApplicationRecord after_commit :send_confirmation_email, on: :create private def send_confirmation_email OrderMailer.confirmation(self).deliver_later end end
Now the email is only enqueued after the transaction commits successfully. If the transaction rolls back, the callback never fires.
Action-Specific Shortcuts
rubyclass Article < ApplicationRecord after_create_commit :on_created after_update_commit :on_updated after_destroy_commit :on_destroyed after_save_commit :on_saved # create or update end
These are syntactic sugar for after_commit :method, on: :create etc.
Handling Rollbacks
rubyclass Payment < ApplicationRecord after_rollback :log_failure after_rollback :notify_admin, on: :create private def log_failure Rails.logger.error "Payment #{id} transaction rolled back" end def notify_admin AdminMailer.payment_failed(self).deliver_later end end
Cache Invalidation
rubyclass Product < ApplicationRecord after_commit :invalidate_cache private def invalidate_cache Rails.cache.delete("product_#{id}") Rails.cache.delete("products_list") end end
Cache invalidation is a perfect use case for after_commit because you want the cache to reflect the committed state.
Common Pitfalls
- Using after_save for external services -- Emails, background jobs, webhooks, and API calls should always use
after_commit. The transaction could roll back afterafter_savefires. - Forgetting that after_commit runs outside the transaction -- Any database operations inside
after_commitrun in a new, separate transaction. - Multiple after_commit on the same action -- If you define multiple
after_update_commitcallbacks, they all run. Make sure they are independent and idempotent.
Best Practices
- Use
after_commitfor all external side effects -- emails, jobs, API calls, WebSocket broadcasts. - Use
after_saveonly for same-transaction side effects -- like updating related records that should roll back together. - Use
after_destroy_commitwhen cleaning up external resources -- file storage, search indexes, CDN cache.
Summary
after_commitruns only after a successful transaction commit.after_rollbackruns only after a transaction rollback.- Use
after_create_commit,after_update_commit,after_destroy_commitfor action-specific hooks. - Always use
after_commit(notafter_save) for emails, background jobs, and external API calls. after_saveruns inside the transaction and can fire for operations that later roll back.
Code Examples
class Order < ApplicationRecord
# WRONG: runs inside transaction, may fire for rolled-back records
# after_save :send_confirmation_email
# CORRECT: runs only after successful commit
after_create_commit :send_confirmation_email
after_update_commit :clear_cache
after_destroy_commit :cleanup_files
private
def send_confirmation_email
OrderMailer.confirmation(self).deliver_later
end
end