{"title":"Callbacks","description":"Lifecycle hooks and callback methods in Grant ORM","section":"guides/models/grant","version":"v2","path":"guides/models/grant/callbacks","canonical_url":"https://amberframework.org/docs/v2/guides/models/grant/callbacks","markdown_url":"https://amberframework.org/docs/v2/guides/models/grant/callbacks.md","inherited":false,"content_markdown":"# Callbacks\n\n> **Supported web path:** Amber CLI `2.0.6` includes Grant in every generated\n> web application and pins the reviewed V2 commit. Preserve that pin while\n> following this beta.\n\n## Where the examples go\n\nCallback declarations and their private methods belong inside the matching\nGrant model under `src/models/`, such as `src/models/user.cr`. Examples that\ninvoke `save`, `destroy`, or a bulk operation run from the controller, job,\nservice, or spec that owns the operation. External delivery belongs in a job or\nservice called after commit. Blocks on this page use those destinations unless\na closer comment identifies a different role.\n\nCallbacks are methods that get called at certain moments of an object's lifecycle. They allow you to trigger logic before or after alterations to your model's state.\n\n## Available Callbacks\n\n### Create Callbacks\n\n```crystal\nclass User < Grant::Base\n  before_validation :set_defaults           # 1. First callback\n  # validations run here                    # 2. Validations\n  after_validation :process_validated_data  # 3. After validation\n  before_save :before_save_tasks           # 4. Before save (create or update)\n  before_create :before_create_tasks       # 5. Before create specifically\n  # INSERT happens here                     # 6. Database insert\n  after_create :after_create_tasks         # 7. After create\n  after_save :after_save_tasks            # 8. After save (create or update)\n  after_commit :after_commit_tasks        # 9. After transaction commits\nend\n```\n\n### Update Callbacks\n\n```crystal\nclass Product < Grant::Base\n  before_validation :normalize_data         # 1. First callback\n  # validations run here                    # 2. Validations\n  after_validation :process_changes        # 3. After validation\n  before_save :before_save_tasks          # 4. Before save\n  before_update :before_update_tasks      # 5. Before update specifically\n  # UPDATE happens here                    # 6. Database update\n  after_update :after_update_tasks        # 7. After update\n  after_save :after_save_tasks           # 8. After save\n  after_commit :after_commit_tasks       # 9. After transaction commits\nend\n```\n\n### Destroy Callbacks\n\n```crystal\nclass Comment < Grant::Base\n  before_destroy :cleanup_associations     # 1. Before destroy\n  # DELETE happens here                    # 2. Database delete\n  after_destroy :log_deletion             # 3. After destroy\n  after_commit :notify_deletion          # 4. After transaction commits\nend\n```\n\n## Callback Registration\n\n### Method Symbols\n\n```crystal\nclass Article < Grant::Base\n  before_save :sanitize_content\n  after_create :publish_to_feed\n\n  private def sanitize_content\n    self.content = Sanitizer.clean(content)\n  end\n\n  private def publish_to_feed\n    FeedService.publish(self) if published?\n  end\nend\n```\n\n### Blocks\n\n```crystal\nclass Order < Grant::Base\n  before_save do\n    self.total = calculate_total\n  end\n\n  after_create do\n    OrderMailer.confirmation(self).deliver_later\n  end\nend\n```\n\n### Conditional Callbacks\n\n```crystal\nclass Post < Grant::Base\n  # With symbol conditions\n  before_save :update_slug, if: :title_changed?\n  after_create :notify_subscribers, if: :published?\n\n  # With proc conditions\n  before_destroy :archive_content,\n    if: ->(post : Post) { post.views > 1000 }\n\n  # Multiple conditions\n  after_save :clear_cache,\n    if: :published?,\n    unless: :draft?\nend\n```\n\n## Common Callback Patterns\n\n### Data Normalization\n\n```crystal\nclass User < Grant::Base\n  before_validation :normalize_fields\n\n  column email : String\n  column phone : String?\n  column name : String\n\n  private def normalize_fields\n    self.email = email.downcase.strip\n    self.phone = phone.try(&.gsub(/\\D/, \"\"))\n    self.name = name.split.map(&.capitalize).join(\" \")\n  end\nend\n```\n\n### Setting Defaults\n\n```crystal\nclass Document < Grant::Base\n  before_create :set_defaults\n\n  column uuid : String\n  column version : Int32\n  column status : String\n\n  private def set_defaults\n    self.uuid ||= UUID.random.to_s\n    self.version ||= 1\n    self.status ||= \"draft\"\n  end\nend\n```\n\n### Generating Tokens\n\n```crystal\nclass Session < Grant::Base\n  before_create :generate_token\n\n  column token : String\n  column expires_at : Time\n\n  private def generate_token\n    loop do\n      self.token = Random::Secure.hex(32)\n      break unless Session.exists?(token: token)\n    end\n    self.expires_at = 24.hours.from_now\n  end\nend\n```\n\n### Slug Generation\n\n```crystal\nclass Article < Grant::Base\n  before_save :generate_slug\n\n  column title : String\n  column slug : String\n\n  private def generate_slug\n    return unless title_changed?\n\n    base_slug = title.downcase.gsub(/[^a-z0-9]+/, \"-\")\n    self.slug = base_slug\n\n    counter = 1\n    while Article.exists?(slug: slug)\n      self.slug = \"#{base_slug}-#{counter}\"\n      counter += 1\n    end\n  end\nend\n```\n\n### Audit Trails\n\n```crystal\nclass AuditableModel < Grant::Base\n  after_create :log_create\n  after_update :log_update\n  after_destroy :log_destroy\n\n  private def log_create\n    AuditLog.create!(\n      model: self.class.name,\n      record_id: id,\n      action: \"create\",\n      user_id: Current.user_id,\n      changes: attributes.to_json\n    )\n  end\n\n  private def log_update\n    return unless changes.any?\n    AuditLog.create!(\n      model: self.class.name,\n      record_id: id,\n      action: \"update\",\n      user_id: Current.user_id,\n      changes: changes.to_json\n    )\n  end\nend\n```\n\n### Cache Management\n\n```crystal\nclass Product < Grant::Base\n  after_save :clear_cache\n  after_destroy :clear_cache\n\n  private def clear_cache\n    Cache.delete(\"product:#{id}\")\n    Cache.delete(\"category:#{category_id}:products\")\n  end\nend\n```\n\n## Halting Execution\n\n### Throwing :abort\n\n```crystal\nclass Order < Grant::Base\n  before_save :check_inventory\n\n  private def check_inventory\n    if total_items > available_stock\n      errors.add(:items, \"Insufficient inventory\")\n      throw :abort  # Halts execution\n    end\n  end\nend\n```\n\n### Preventing Destruction\n\n```crystal\nclass User < Grant::Base\n  before_destroy :prevent_admin_deletion\n\n  private def prevent_admin_deletion\n    if admin? && User.where(admin: true).count == 1\n      errors.add(:base, \"Cannot delete the last admin\")\n      throw :abort\n    end\n  end\nend\n```\n\n## Transaction Callbacks\n\n### after_commit\n\nRuns after the database transaction successfully commits:\n\n```crystal\nclass Order < Grant::Base\n  after_commit :send_confirmation, on: :create\n  after_commit :update_inventory, on: :update\n\n  private def send_confirmation\n    # Safe to send email - transaction committed\n    OrderMailer.confirmation(self).deliver_later\n  end\n\n  private def update_inventory\n    # Safe to call external services\n    InventoryService.sync(self)\n  end\nend\n```\n\n### after_rollback\n\nRuns if the database transaction is rolled back:\n\n```crystal\nclass Payment < Grant::Base\n  after_rollback :log_failure\n\n  private def log_failure\n    Log.error { \"Payment #{id} failed: #{errors.full_messages}\" }\n  end\nend\n```\n\n## Performance Considerations\n\n### Keep Callbacks Fast\n\n```crystal\nclass Post < Grant::Base\n  # Bad: Synchronous external call\n  after_create :notify_external_service\n\n  private def notify_external_service\n    HTTPClient.post(\"https://api.example.com/webhook\", body: to_json)\n  end\n\n  # Good: Queue for background processing\n  after_create :queue_notification\n\n  private def queue_notification\n    NotificationJob.perform_later(self.id)\n  end\nend\n```\n\n### Use Conditional Callbacks\n\n```crystal\nclass User < Grant::Base\n  # Only run expensive callbacks when necessary\n  after_save :sync_to_crm, if: :crm_fields_changed?\n\n  private def crm_fields_changed?\n    (changes.keys & [\"email\", \"name\", \"company\"]).any?\n  end\nend\n```\n\n## Skipping Callbacks\n\n```crystal\n# Skip callbacks when needed\nuser.save(skip_callbacks: true)\n\n# Bulk operations skip callbacks\nUser.update_all(active: false)\nuser.update_columns(name: \"New\")  # Direct SQL, no callbacks\n```\n\n## Best Practices\n\n### 1. Keep Callbacks Simple\n\n```crystal\n# Good: Single responsibility\nbefore_save :normalize_email\nbefore_save :hash_password\nbefore_save :set_defaults\n\n# Bad: Doing too much\nbefore_save :do_everything\n```\n\n### 2. Use Appropriate Callback\n\n```crystal\n# Good: after_commit for external services\nafter_commit :send_email\n\n# Bad: after_save might run even if rolled back\nafter_save :send_email\n```\n\n### 3. Consider Service Objects\n\n```crystal\n# Instead of complex callbacks\nclass User < Grant::Base\n  after_create :setup_user_account\n\n  private def setup_user_account\n    UserAccountSetupService.new(self).perform\n  end\nend\n\nclass UserAccountSetupService\n  def initialize(@user : User)\n  end\n\n  def perform\n    create_profile\n    send_welcome_email\n    assign_default_role\n  end\nend\n```"}