Rails Callbacks: Your Silent Helpers (and Hidden Side-Effects)

Rails Callbacks: Your Silent Helpers (and Hidden Side-Effects)

posted 3 min read

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.


If you read this far, tweet to the author to show them you care. Tweet a Thanks
0 votes
0 votes

More Posts

The One-Letter Rails Bug That Slipped Past Rubocop, CI, and Code Reviews

Madhu Hari - Sep 3

The Hidden Power of Your Test Setup and Mocks

Waffeu Rayn - Aug 27

Getting Started with Ruby & Rails on Windows

Sunny - Aug 2

How to apply various postprocessing effects like Bloom, Motion Blur, and Pixelation inside a Vue

Sunny - Jul 6

ShipWithAI #9 — Validate Your Side Project Idea in 30 Minutes

Alessandro Magionami - Oct 6
chevron_left