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
class Post < ApplicationRecord
belongs_to :author, class_name: "User", touch: true
has_many :comments, dependent: :destroy
validates :title, presence: true
scope :published, -> { where.not(published_at: nil) }
# Included in the fragment cache key so a schema/template change
# can force a global rebuild without a data migration.
def cache_version
"#{updated_at.to_i}-v3"
end
def comment_count
comments.visible.size
end
end
<% cache [post, "post-shell"] do %>
<article class="post" id="<%= dom_id(post) %>">
<header>
<h2><%= link_to post.title, post %></h2>
<span class="byline">by <%= post.author.display_name %></span>
</header>
<div class="body"><%= sanitize post.body %></div>
<section class="comments" data-count="<%= post.comment_count %>">
<% post.comments.visible.each do |comment| %>
<% cache comment do %>
<div class="comment" id="<%= dom_id(comment) %>">
<strong><%= comment.author.display_name %></strong>
<p><%= simple_format(comment.body) %></p>
<% if comment.edited? %>
<em class="edited">(edited)</em>
<% end %>
</div>
<% end %>
<% end %>
</section>
</article>
<% end %>
class PostsController < ApplicationController
def show
@post = Post.published
.includes(:author, comments: :author)
.find(params[:id])
fresh_when(@post)
end
def index
@posts = Post.published
.includes(:author)
.order(published_at: :desc)
.page(params[:page])
end
end
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
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
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
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
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
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
<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)
Share this code
Here's the card — post it anywhere.