Here is an idea I had - using rails ActiveRecord validations to ensure that the proper methods are used to update state.
Part of the allure of active record callbacks (after_update_commit,
before_destroy etc) is that you don’t need to worry about remembering to run
them.
For example, if updating a Comment should kick off a job to notify the
Author of a BlogPost via email, well, just use something like:
class Comment < ApplicationRecord
  # ... 
 def notify_blog.author_of_new_verified_comment
    return unless poster.verified?
    NotifyAuthorOfCommentJob.perform_async(self.id, blog.author.id)
 end
 after_save :notify_author_of_new_verified_comment
end
Then in any controller, we don’t need to worry about making sure we remember to enqueue the notify job, but only if the comment poster is a verified account etc.
This becomes a more comfortable crutch to lean upon as the number of contributers (or just the sheer codebase size) increases. A proliferation of callbacks can (not always, but can) lead to pain - you can spend a lot of time trying to figure out complex happy-paths that end up being reliant on several callbacks chained together.
I’ve long liked the idea of using specific objects and/or methods to encapsulate
behavior that needs to happen when changing some state. Things like a
BlogPost#publish! method that not only updates the post’s status to
published, but also increases the authors post_count, and kicks off a job to
rebuild some post cache somewhere. Maybe it even sends off some emails.
This is often easier to test, makes it clear where to add future behavior that may be needed, and is conceptually clear - if I want a set of things to happen when a post is published, that is the responsibility of the ‘Publisher’.
BUT… how do we make sure that the BlogPublisher is used consistently? Maybe we
add a shortcut action somewhere, and instead of remembering to call the
BlogPublisher, we only call @post.update(status: :published). If we had an
ActiveRecord callback, we wouldn’t need to worry about that, right?
Well, here is a crazy idea (It may also be terrible, I haven’t used this pattern
in anger): Let’s use AR validations to raise an error if something other than
the Post.publish! method is used to set the status column to :published
Here is an example:
class Post < ApplicationRecord
  attr_reader :in_publish_flow
  class ImproperUpdateError < StandardError; end
  # ...
  validate do 
    unless @in_publish_flow 
      || !will_save_change_to_attribute?(:status, to: :published)
      raise ImproperUpdateError, "use Post#publish! to publish a blog post" 
    end
  end
  
  def publish!
    @in_publish_flow = true
    update(status: :published)
    author.increment_post_count! 
    RebuildBlogPostCacheJob.perform_later(author.id)
  end
end
Notice that we’re raising an error, instead of the usual
errors.add(:status, "...") seen in validations. This is intentional, because
we want this to be caught in development, this isn’t something we want to
bubble up to the users of our app1.
Also, we’re using an attr_reader for in_publish_flow, and that value is only set
in the publish! method - this attr being set to true is the guard against us
just calling update(..) instead of using the proper method
In the end, if we really want/need to, we can still just call
post.update_attribute(status: "published") (this is ruby, after all, there is 
always a workaround), but at least at that point it becomes a conscious choice,
and we aren’t doing this by accident.
I fully acknowledge that this in itself might be a smell ↩