Skip to main content

Using Rails Callbacks for Data Consistency with Multiple Associated Models

Published: February 28, 2017 Updated: June 18, 2026 Larry Qu 2 min read

Using Callbacks for Associated Model Consistency

Rails callbacks help maintain data consistency across associated models. When an Order is created, corresponding OrderItem records must be generated from the user’s cart. When an Order is destroyed, its OrderItems should be cleaned up.

Schema Setup

Two models, Order and OrderItem, share an orderID field for association:

create_table "order_items", force: :cascade do |t|
  t.string   "orderID"
  t.integer  "product_id"
  t.decimal  "price",      precision: 10, scale: 2
  t.decimal  "amount",     precision: 10, scale: 2
  t.datetime "created_at",                          null: false
  t.datetime "updated_at",                          null: false
  t.integer  "user_id"
  t.index ["product_id"], name: "index_order_items_on_product_id", using: :btree
end

create_table "orders", force: :cascade do |t|
  t.integer  "user_id"
  t.string   "orderID"
  t.text     "message"
  t.datetime "created_at",                             null: false
  t.datetime "updated_at",                             null: false
  t.string   "fixed_address"
  t.decimal  "total_price",   precision: 10, scale: 2
  t.index ["user_id"], name: "index_orders_on_user_id", using: :btree
end

The key requirement: when an Order is placed, OrderItems must be created from the current cart. When an Order is deleted, its OrderItems must cascade.

Callback Implementation

class Order < ApplicationRecord
  belongs_to :user
  belongs_to :address

  after_create :create_order_items
  after_destroy :destroy_all_same_orderID

  protected
    def create_order_items
      current_user = self.user
      current_user.carts.each do |cart|
        OrderItem.new(
          user_id: current_user.id,
          orderID: self.orderID,
          product_id: cart.product_id,
          price: cart.product.price,
          amount: cart.amount
        ).save!
      end
      Cart.where(user_id: current_user.id).destroy_all
    end

    def destroy_all_same_orderID
      OrderItem.where(orderID: self.orderID).destroy_all
    end
end

Using save! (with bang) ensures the operation fails loudly if validation fails, preventing partial order creation.

Controller Integration

In the OrdersController#create action, you simply build and save the Order. The callback handles OrderItem creation automatically, keeping controller logic thin:

class OrdersController < ApplicationController
  def create
    @order = Order.new(order_params)
    if @order.save
      redirect_to @order, notice: 'Order created successfully.'
    else
      render :new
    end
  end
end

Callbacks are declared as protected or private to prevent external callers from invoking them directly. This encapsulation maintains a clear boundary around the model’s lifecycle logic.

Avoiding Callback Hell

Callbacks become problematic when they chain across multiple models or have conditional side effects. Follow these guidelines to keep them manageable:

  • Keep callbacks focused on the model’s own data. An Order callback should create OrderItems, but should not send emails or charge credit cards — use service objects or background jobs for those.
  • Prefer after_create over before_create when the operation depends on the record being persisted.
  • Use has_many dependent callbacks (dependent: :destroy) instead of manual after_destroy where the relationship is a simple foreign key.
  • Extract complex callback logic into service objects. For example, an OrderProcessor service can handle cart-to-order conversion and callbacks independently.

Resources

Comments

👍 Was this article helpful?