ruby erb 78 lines · 4 tabs

Granular Cache Invalidation with touch: true

Shared by codesnips Jan 2026
4 tabs
class Comment < ApplicationRecord
  belongs_to :post, touch: true
  belongs_to :author, class_name: "User"

  validates :body, presence: true, length: { maximum: 10_000 }

  scope :visible, -> { where(hidden: false).order(created_at: :asc) }

  after_commit :broadcast_refresh, on: [:create, :update, :destroy]

  def edited?
    updated_at - created_at > 1.second
  end

  private

  def broadcast_refresh
    PostChannel.broadcast_to(post, action: "refresh", comment_id: id)
  end
end
4 files · ruby, erb Explain with highlit

This snippet shows how Rails implements Russian doll fragment caching, where nested cached views are invalidated automatically by cascading updated_at timestamps through touch: true associations. The core problem is keeping cached HTML fresh: when a child record changes, every cached fragment that renders it — and every parent fragment that wraps those children — must be regenerated, but nothing else should be discarded. Timestamp-based cache keys plus association touching solve this without any manual expire_fragment bookkeeping.

In Comment model, belongs_to :post, touch: true is the linchpin. Whenever a Comment is created, updated, or destroyed, ActiveRecord bumps the parent Post's updated_at. Because Rails cache keys derived from a record include its class name, id, and updated_at (via cache_key_with_version), bumping the parent's timestamp changes the parent's cache key, which invalidates the outer fragment. The after_commit callback that calls broadcast_refresh is a common companion, but the caching correctness comes entirely from touch: true.

In Post model, touch: true on belongs_to :author extends the chain one level higher, and has_many :comments completes the graph. Editing a comment touches its post; touching the post touches the author. This means a fragment keyed on the author naturally expires too, which is exactly the desired cascade.

_post partial demonstrates the nesting. The outer cache post block wraps an inner cache comment loop. When one comment changes, only that comment's inner fragment and the post's outer wrapper are recomputed; sibling comment fragments are reused from cache because their individual keys are unchanged. This reuse is what makes Russian doll caching fast — most of the inner work is skipped on a rebuild.

The main trade-off is write amplification: every child write issues an extra UPDATE per touched ancestor, which can become a hotspot under heavy write load or deep hierarchies. Counter-caches and touching can also interact with updated_at in surprising ways during bulk imports, so operations that intentionally skip callbacks (update_column, insert_all) will silently leave stale fragments. This pattern shines for read-heavy pages with occasional edits, such as blog posts, threaded discussions, or dashboards.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Granular Cache Invalidation with touch: true — share card
Link copied