module Cacheable
extend ActiveSupport::Concern
def cache_query(prefix, scope, expires_in: 15.minutes)
key = [prefix, cache_version_token(scope)].join("/")
Rails.cache.fetch(key, expires_in: expires_in, race_condition_ttl: 10.seconds) do
yield
end
end
private
def cache_version_token(scope)
relation = scope.unscope(:order)
max = relation.maximum(:updated_at)
stamp = max ? max.utc.to_f.to_s : "0"
"#{relation.count}-#{stamp}"
end
end
class LeaderboardQuery
include Cacheable
def initialize(season_id:, since: nil, limit: 50)
@season_id = season_id
@since = since
@limit = limit
end
def records
prefix = "leaderboard/season-#{@season_id}/since-#{@since.to_i}/top-#{@limit}"
cache_query(prefix, scope) do
scope
.group(:player_id)
.order("total_points DESC")
.limit(@limit)
.pluck(Arel.sql("player_id, SUM(points) AS total_points"))
.map { |player_id, points| { player_id: player_id, points: points } }
end
end
private
def scope
rel = Score.where(season_id: @season_id)
rel = rel.where("created_at >= ?", @since) if @since
rel
end
end
class LeaderboardsController < ApplicationController
def index
query = LeaderboardQuery.new(
season_id: params.require(:season_id),
since: parse_since(params[:since]),
limit: [params.fetch(:limit, 50).to_i, 200].min
)
records = query.records
if stale?(etag: records, public: true)
render json: { leaderboard: records }
end
end
private
def parse_since(value)
return if value.blank?
Time.zone.parse(value)
rescue ArgumentError
nil
end
end
This snippet shows how an expensive read-heavy endpoint in Rails can be cached with a versioned key strategy that survives model updates without manual key bookkeeping. The core problem is that a leaderboard-style aggregation query is slow and hit constantly, but its results only change when the underlying records change. Rather than caching the whole HTTP response, the caching is scoped to the query result, keyed off data that naturally advances when the data mutates.
In Cacheable concern, cache_query wraps Rails.cache.fetch and builds the key from a caller-supplied prefix plus a cache_version_token. That token is a maximum(:updated_at) combined with a count, so any insert, update, or delete on the scope produces a new key and the old entry is simply orphaned instead of needing explicit deletion. The race_condition_ttl option is important: it lets one process keep serving a slightly-stale value while another recomputes, preventing a thundering herd from hammering the database the instant a popular key expires.
In LeaderboardQuery, the actual aggregation lives in records, a plain query object that groups scores and orders them. It calls cache_query with a prefix that includes the season_id, so different seasons occupy distinct keys and invalidate independently. The since filter is folded into the key too, ensuring different time windows never collide. Keeping the SQL in a query object keeps the concern generic and the controller thin.
In LeaderboardsController, index delegates entirely to the query object and renders JSON. Because caching happens beneath the controller, the same cached data can be reused by jobs, exports, or other endpoints without duplicating cache logic. HTTP-level stale? handling with an etag is layered on top so unchanged responses short-circuit with a 304.
The main trade-off is that maximum(:updated_at) and count are themselves queries, though far cheaper than the aggregation they guard, and they can be pushed into a covering index. A pitfall to watch is clock precision: if updated_at lacks sub-second resolution, two rapid writes could share a token, which is why count is mixed in as a tiebreaker. This pattern fits endpoints where reads vastly outnumber writes and exact freshness within milliseconds is not required.
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.