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.
92
90%
Does it follow best practices?
Impact
100%
1.09xAverage score across 3 eval scenarios
Passed
No findings from the security scan
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
endRails.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
endclass 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
endclass 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
endclass 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_countclass 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
endRSpec.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
endRSpec.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]}"
endtouch: true on belongs_to for Russian dollcached: truefernandezbaptiste/rails_ai_agents
15fdeaf
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.