When you’re building features in Ruby on Rails, few things feel as convenient as Active Record callbacks. They’re like silent assistants: “after you save, I’ll send the email,” or “before you destroy, I’ll clean up the data.”
But those same helpers can quietly become the source of bugs, performance issues, and late-night debugging. Let’s walk through what callbacks are, how to use them wisely, and where to be cautious — with real-life examples from production Rails apps.
What are callbacks?
Callbacks are hooks into the lifecycle of an Active Record object. They let you run logic automatically at specific moments:
- Before validation
- After saving
- After committing a transaction
- Before destroying
Example:
class Booking < ApplicationRecord
before_validation :normalize_dates
after_commit :send_confirmation_email, on: :create
private
def normalize_dates
self.start_date = start_date&.to_date
self.end_date = end_date&.to_date
end
def send_confirmation_email
BookingMailer.confirmation(self).deliver_later
end
end
Here, Rails guarantees:
- Before validation: dates are cleaned.
- After the record is safely persisted: the confirmation email job is queued.
A real-life situation: reports gone wild
In one project, we had:
class Booking < ApplicationRecord
after_commit :create_reports
private
def create_reports
ReportsWorker.perform_async
end
end
It looked harmless. But here’s what happened:
- On create: ✅ We wanted yearly reports.
- On update: ❌ Reports were re-generated every time, even if you only changed the guest name.
- On destroy: ❌ Still ran, queuing reports for a deleted booking!
The silent helper turned into hidden chaos.
Fix: scope it properly:
after_commit :create_yearly_reports, on: :create
or, if you need it on updates but only when dates change:
after_commit :create_reports, on: :update, if: :dates_changed?
def dates_changed?
saved_change_to_start_date? || saved_change_to_end_date?
end
The hidden side-effects
Callbacks can introduce subtle issues:
1. Performance hits
Every save might enqueue background jobs, send emails, or touch external APIs. Developers might not realize that a simple booking.save! is doing much more than writing to the DB.
2. Complexity creep
Business logic ends up scattered across models. To understand “what happens when I update a booking,” you now have to read through a maze of callbacks.
3. Surprising order of execution
Did you know after_commit fires once per transaction (after commit), while after_save fires immediately on each save inside the transaction? Subtle, but it matters if you’re queuing jobs or updating counters.
When callbacks shine
They’re not evil — they shine when used for model housekeeping:
- Normalization (
before_validation)
- Cleaning up associations (
before_destroy)
- Maintaining consistency (e.g., updating a denormalized column)
Example: ensuring slugs are unique and normalized:
class Article < ApplicationRecord
before_validation :generate_slug
private
def generate_slug
self.slug ||= title.parameterize
end
end
When to move logic elsewhere
For business processes (emails, payments, reporting, syncing external APIs), callbacks are often the wrong place. Better alternatives:
- Service objects: Encapsulate business workflows.
- Explicit calls: Let controllers or services trigger jobs.
Example: Instead of:
after_commit :notify_crm
Prefer:
class BookingCreator
def call(params)
booking = Booking.create!(params)
CrmNotifier.new(booking).deliver
booking
end
end
Now it’s explicit: creating a booking always notifies the CRM.
Practical tips
- Use
on: :create, on: :update, on: :destroy to scope callbacks.
- Use
if: / unless: guards to avoid unnecessary triggers.
- Keep callbacks idempotent — safe to run multiple times.
- Keep them fast — heavy work should be backgrounded.
- Document them in the model (
# Callbacks section) so new devs know what’s happening.
Wrapping up
Rails callbacks are powerful allies for automating model lifecycle tasks. But they’re double-edged swords: silent helpers that can hide side-effects and surprise you in production.
The rule of thumb:
- Housekeeping in callbacks (normalize, clean up).
- Business workflows in services/observers (emails, billing, reporting).
Use them wisely, and your Rails app will stay maintainable — without those “why did this email send 3 times?” mysteries.