skills/caching-strategies/SKILL.md
Implements Rails caching patterns for performance optimization. Use when adding fragment caching, Russian doll caching, low-level caching, cache invalidation, or when user mentions caching, performance, cache keys, or memoization.
npx skillsauth add fernandezbaptiste/rails_ai_agents caching-strategiesInstall 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.
Rails provides multiple caching layers:
# config/environments/development.rb
config.action_controller.perform_caching = true
config.cache_store = :memory_store
# config/environments/production.rb
config.cache_store = :solid_cache_store # Rails 8 default
# OR
config.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"] }
Enable caching in development:
bin/rails dev:cache
| Store | Use Case | Pros | Cons |
|-------|----------|------|------|
| :memory_store | Development | Fast, no setup | Not shared, limited size |
| :solid_cache_store | Production (Rails 8) | Database-backed, no Redis | Slightly slower |
| :redis_cache_store | Production | Fast, shared | Requires Redis |
| :file_store | Simple production | Persistent, no Redis | Slow, not shared |
| :null_store | Testing | No caching | N/A |
<%# app/views/events/_event.html.erb %>
<% cache event do %>
<article class="event-card">
<h3><%= event.name %></h3>
<p><%= event.description %></p>
<time><%= l(event.event_date, format: :long) %></time>
<%= render event.venue %>
</article>
<% end %>
Rails generates cache keys from:
updated_at timestamp# Generated key example:
# views/events/123-20240115120000000000/abc123digest
<%# With version %>
<% cache [event, "v2"] do %>
...
<% end %>
<%# With user-specific content %>
<% cache [event, current_user] do %>
...
<% end %>
<%# With explicit key %>
<% cache "featured-events-#{Date.current}" do %>
<%= render @featured_events %>
<% end %>
Nested caches where inner caches are reused when outer cache is invalidated:
<%# app/views/events/show.html.erb %>
<% cache @event do %>
<h1><%= @event.name %></h1>
<section class="vendors">
<% @event.vendors.each do |vendor| %>
<% cache vendor do %>
<%= render partial: "vendors/card", locals: { vendor: vendor } %>
<% end %>
<% end %>
</section>
<section class="comments">
<% @event.comments.each do |comment| %>
<% cache comment do %>
<%= render comment %>
<% end %>
<% end %>
</section>
<% end %>
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :event, touch: true # Updates event.updated_at when comment changes
end
# app/models/event_vendor.rb
class EventVendor < ApplicationRecord
belongs_to :event, touch: true
belongs_to :vendor
end
<%# Caches each item individually %>
<%= render partial: "events/event", collection: @events, cached: true %>
<%# Equivalent to: %>
<% @events.each do |event| %>
<% cache event do %>
<%= render event %>
<% end %>
<% end %>
<%= render partial: "events/event",
collection: @events,
cached: ->(event) { [event, current_user.admin?] } %>
# Read with block (fetch)
Rails.cache.fetch("stats/#{Date.current}", expires_in: 1.hour) do
# Expensive calculation
{
total_events: Event.count,
total_revenue: Order.sum(:total_cents)
}
end
# Just read (returns nil if missing)
stats = Rails.cache.read("stats/#{Date.current}")
# Just write
Rails.cache.write("stats/#{Date.current}", stats, expires_in: 1.hour)
# Delete
Rails.cache.delete("stats/#{Date.current}")
# app/services/dashboard_stats_service.rb
class DashboardStatsService
CACHE_KEY = "dashboard_stats"
CACHE_TTL = 15.minutes
def call(account:)
Rails.cache.fetch(cache_key(account), expires_in: CACHE_TTL) do
calculate_stats(account)
end
end
def invalidate(account:)
Rails.cache.delete(cache_key(account))
end
private
def cache_key(account)
"#{CACHE_KEY}/#{account.id}"
end
def calculate_stats(account)
{
events_count: account.events.count,
upcoming_events: account.events.upcoming.count,
total_revenue: calculate_revenue(account)
}
end
end
# app/queries/dashboard_stats_query.rb
class DashboardStatsQuery
def initialize(account:, use_cache: true)
@account = account
@use_cache = use_cache
end
def upcoming_events(limit: 5)
return fetch_upcoming_events(limit) unless @use_cache
Rails.cache.fetch(cache_key("upcoming", limit), expires_in: 5.minutes) do
fetch_upcoming_events(limit)
end
end
private
def cache_key(type, *args)
"dashboard/#{@account.id}/#{type}/#{args.join('-')}"
end
def fetch_upcoming_events(limit)
@account.events.upcoming.limit(limit).to_a
end
end
Rails.cache.fetch("key", expires_in: 1.hour) { ... }
# Cache key includes timestamp, auto-expires when model changes
cache_key = "event/#{event.id}-#{event.updated_at.to_i}"
Rails.cache.fetch(cache_key) { ... }
# In model callback
class Event < ApplicationRecord
after_commit :invalidate_caches
private
def invalidate_caches
Rails.cache.delete("featured_events")
Rails.cache.delete_matched("dashboard/#{account_id}/*")
end
end
# In service
class Events::UpdateService
def call(event, params)
event.update!(params)
invalidate_related_caches(event)
success(event)
end
private
def invalidate_related_caches(event)
Rails.cache.delete("event_count/#{event.account_id}")
DashboardStatsService.new.invalidate(account: event.account)
end
end
# Delete all keys matching pattern (Redis only)
Rails.cache.delete_matched("dashboard/*")
# For Solid Cache / Memory Store, use namespaced keys
Rails.cache.delete("dashboard/#{account_id}/stats")
Rails.cache.delete("dashboard/#{account_id}/events")
class EventsController < ApplicationController
def show
@event = Event.find(params[:id])
# Returns 304 Not Modified if unchanged
if stale?(@event)
respond_to do |format|
format.html
format.json { render json: @event }
end
end
end
def index
@events = current_account.events.recent
# With custom ETag
if stale?(etag: @events, last_modified: @events.maximum(:updated_at))
render :index
end
end
end
class Api::EventsController < Api::BaseController
def show
@event = Event.find(params[:id])
# Public caching (CDN can cache)
expires_in 1.hour, public: true
# Private caching (browser only)
expires_in 15.minutes, private: true
render json: @event
end
end
class EventPresenter < BasePresenter
def vendor_count
@vendor_count ||= event.vendors.count
end
def total_cost
@total_cost ||= calculate_total_cost
end
private
def calculate_total_cost
event.event_vendors.sum(:amount_cents)
end
end
class Current < ActiveSupport::CurrentAttributes
attribute :dashboard_stats
def dashboard_stats
super || self.dashboard_stats = DashboardStatsQuery.new(user: user).call
end
end
# Migration
add_column :events, :vendors_count, :integer, default: 0, null: false
# Model
class Vendor < ApplicationRecord
belongs_to :event, counter_cache: true
end
# Usage (no query needed)
event.vendors_count
class Event < ApplicationRecord
after_commit :update_account_counters
private
def update_account_counters
account.update_columns(
events_count: account.events.count,
active_events_count: account.events.active.count
)
end
end
# spec/rails_helper.rb
RSpec.configure do |config|
config.around(:each, :caching) do |example|
caching = ActionController::Base.perform_caching
ActionController::Base.perform_caching = true
Rails.cache.clear
example.run
ActionController::Base.perform_caching = caching
end
end
RSpec.describe "Events", type: :request, :caching do
it "caches the event show page" do
event = create(:event)
# First request - cache miss
get event_path(event)
expect(response.body).to include(event.name)
# Update event
event.update!(name: "New Name")
# Second request - should show new name (cache invalidated)
get event_path(event)
expect(response.body).to include("New Name")
end
end
RSpec.describe DashboardStatsService do
describe "#invalidate" do
it "clears the cache" do
account = create(:account)
service = described_class.new
# Prime cache
service.call(account: account)
# Invalidate
service.invalidate(account: account)
# Verify cache miss
expect(Rails.cache.exist?("dashboard_stats/#{account.id}")).to be false
end
end
end
# config/environments/production.rb
config.action_controller.enable_fragment_cache_logging = true
# Subscribe to cache events
ActiveSupport::Notifications.subscribe("cache_read.active_support") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
Rails.logger.info "Cache #{event.payload[:hit] ? 'HIT' : 'MISS'}: #{event.payload[:key]}"
end
touch: true on belongs_to for Russian dollcached: truedevelopment
Creates ViewComponents for reusable UI elements with TDD. Use when building reusable UI components, extracting complex partials, creating cards/tables/badges/modals, or when user mentions ViewComponent, components, or reusable UI.
development
Guides Test-Driven Development workflow with Red-Green-Refactor cycle. Use when the user wants to implement a feature using TDD, write tests first, follow test-driven practices, or mentions red-green-refactor.
data-ai
Configures Solid Queue for background jobs in Rails 8. Use when setting up background processing, creating background jobs, configuring job queues, or migrating from Sidekiq to Solid Queue.
testing
Creates service objects following single-responsibility principle with comprehensive specs. Use when extracting business logic from controllers, creating complex operations, implementing interactors, or when user mentions service objects or POROs.