.claude_37signals/skills/caching-patterns/SKILL.md
Implements HTTP caching with ETags, fragment caching, Russian doll caching, and Solid Cache configuration. Use when optimizing performance, adding caching layers, or when user mentions ETags, fresh_when, stale?, cache keys, or Russian doll caching. WHEN NOT: For Turbo Stream real-time updates (use turbo-patterns), for background job cache warming logic (use job-patterns).
npx skillsauth add ThibautBaissac/rails_ai_agents caching-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
fresh_when for free 304 Not Modified responsestouch: true for automatic cache invalidationupdated_at timestampscache_collection for listsRails.cache.fetch for expensive computationsStack: Solid Cache (database-backed), Turbo for page refreshes, ETags with conditional GET, fragment caching in ERB views, collection caching for lists.
Multi-tenancy: Cache keys scoped to account. URL-based:
app.myapp.com/123/projects/456.
Commands:
rails solid_cache:install # Install Solid Cache
rails db:migrate # Run cache migrations
rails cache:clear # Clear cache
Apply caching in this order (highest impact first):
fresh_when / stale? in controllers (free 304s)cache blocks in views (Russian doll)cache_collection for lists of partialsRails.cache.fetch for expensive computationsSee @references/http-caching.md for full details.
# Single resource -- returns 304 if ETag matches
class BoardsController < ApplicationController
def show
@board = Current.account.boards.find(params[:id])
fresh_when @board
end
def index
@boards = Current.account.boards.includes(:creator)
fresh_when @boards
end
end
# Composite ETag from multiple objects
def show
fresh_when [@board, @card, Current.user]
end
# API with stale? for conditional rendering
def show
@board = Current.account.boards.find(params[:id])
if stale?(@board)
render json: @board
end
end
# Custom ETag with parameters
fresh_when etag: [@activities, @report_date, Current.user.timezone]
See @references/fragment-caching.md for full details.
Set up touch cascades in models:
class Card < ApplicationRecord
belongs_to :board, touch: true
end
class Comment < ApplicationRecord
belongs_to :card, touch: true
# Updating comment touches card -> touches board -> invalidates all caches
end
Nest cache blocks in views:
<% cache @board do %>
<h1><%= @board.name %></h1>
<% @board.columns.each do |column| %>
<% cache column do %>
<% column.cards.each do |card| %>
<% cache card do %>
<%= render card %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<%# Cache each item individually with multi-fetch optimization %>
<% cache_collection @boards, partial: "boards/board" %>
<%# Manual alternative %>
<% @boards.each do |board| %>
<% cache board do %>
<%= render "boards/board", board: board %>
<% end %>
<% end %>
Use counter caches to avoid N+1 in cache keys:
class Card < ApplicationRecord
belongs_to :board, counter_cache: true, touch: true
end
class Board < ApplicationRecord
def cache_key_with_version
"#{cache_key}/cards-#{cards_count}-#{updated_at.to_i}"
end
end
<%# Multiple dependencies %>
<% cache ["board_header", @board, Current.user] do %>
<h1><%= @board.name %></h1>
<% if Current.user.can_edit?(@board) %>
<%= link_to "Edit", edit_board_path(@board) %>
<% end %>
<% end %>
<%# With expiration %>
<% cache ["board_stats", @board], expires_in: 15.minutes do %>
<div class="stats"><%= @board.cards.count %> cards</div>
<% end %>
<%# Conditional caching %>
<% cache_if @enable_caching, board do %>
<%= board.name %>
<% end %>
<%# Multi-key with locale %>
<% cache ["dashboard", Current.account, Current.user,
@boards.maximum(:updated_at), I18n.locale] do %>
<%= render "boards_summary", boards: @boards %>
<% end %>
class Board < ApplicationRecord
def statistics
Rails.cache.fetch([self, "statistics"], expires_in: 1.hour) do
{
total_cards: cards.count,
completed_cards: cards.joins(:closure).count,
total_comments: cards.joins(:comments).count
}
end
end
# Race condition protection for expensive operations
def expensive_calculation
Rails.cache.fetch(
[self, "expensive_calculation"],
expires_in: 1.hour,
race_condition_ttl: 10.seconds
) { calculate_complex_metrics }
end
# Version-based cache busting
STATS_VERSION = 2
def versioned_statistics
Rails.cache.fetch([self, "statistics", "v#{STATS_VERSION}"],
expires_in: 1.hour) { calculate_statistics }
end
end
See @references/cache-invalidation.md for full details.
# Prefer touch: true cascades (automatic)
belongs_to :board, touch: true
# Manual invalidation for low-level caches
class Card < ApplicationRecord
after_create_commit :clear_board_caches
after_destroy_commit :clear_board_caches
private
def clear_board_caches
Rails.cache.delete([board, "statistics"])
Rails.cache.delete([board, "card_distribution"])
end
end
# Sweeper pattern for batch invalidation
class CacheSweeper
def self.clear_board_caches(board)
Rails.cache.delete([board, "statistics"])
Rails.cache.delete([board, "card_distribution"])
Rails.cache.delete([board, "activity_summary", Date.current])
end
end
# config/environments/production.rb
config.cache_store = :solid_cache_store
# config/environments/development.rb
config.cache_store = :memory_store, { size: 64.megabytes }
# config/environments/test.rb
config.cache_store = :null_store
class CacheWarmerJob < ApplicationJob
queue_as :low_priority
def perform(account)
account.boards.find_each do |board|
board.statistics
board.card_distribution
end
end
end
# config/recurring.yml
cache:
daily_refresh:
class: DailyCacheRefreshJob
schedule: every day at 3am
queue: low_priority
fresh_when for index and show actionstouch: true on associations for automatic invalidationexpires_in for time-based datatouch: true with Russian doll caching:null_store)development
Creates Turbo Streams, Turbo Frames, and morphing patterns for real-time UI updates. Use when adding real-time updates, partial page rendering, form submissions, or broadcasting. WHEN NOT: For Stimulus JavaScript controllers (see stimulus-patterns skill). For general view conventions (see rules/views.md).
testing
Writes Minitest tests with fixtures following 37signals conventions. Uses Minitest (not RSpec) and fixtures (not factories). Use when writing tests, adding test coverage, or creating fixtures. WHEN NOT: For RSpec or FactoryBot patterns (this project uses Minitest + fixtures exclusively). For test configuration/CI setup (see project docs).
tools
Builds focused, single-purpose Stimulus controllers for progressive enhancement. Use when adding JavaScript behavior, UI interactions, form enhancements, or building reusable client-side components. WHEN NOT: For Turbo Stream/Frame patterns (see turbo-patterns skill). For server-side view logic (see rules/views.md).
testing
Implements the state-as-records-not-booleans pattern for rich state tracking. Use when modeling state changes, replacing boolean flags with record-based state, or when user mentions state records, closures, publications, or toggling state. WHEN NOT: Technical flags like cached/processed (use booleans), concern extraction (use concern-patterns), general model work (use model-patterns).