Russian Doll Caching in Rails: A Complete Guide to Fragment Caching Optimization
Fragment caching in Ruby on Rails, particularly using the Russian doll pattern, is a powerful optimization technique that can significantly improve your application's performance.
This approach, named after the nested nature of Russian matryoshka dolls, allows you to cache nested fragments of your views while maintaining the flexibility to invalidate specific portions when needed.
What is Russian Doll Caching?
Russian doll caching is a caching strategy where cached fragments are nested within other cached fragments, similar to how Russian matryoshka dolls fit inside one another.
When implemented correctly, this pattern can dramatically reduce server processing time and database queries.
The key benefits include:
Granular cache invalidation
Efficient handling of nested relationships
Reduced database queries
Improved response times for complex views
How It Works
Rails uses a cache key generation system that includes:
The model name
The record ID
The
updated_at
timestampAny dependencies specified through
touch: true
associations
When any record is updated, its timestamp changes, automatically invalidating its cache key and all parent caches that contain it.
Cache Key Strategies
Default Rails Cache Keys
By default, Rails generates cache keys using the following pattern:
cache_key = "#{model_name}/#{id}-#{updated_at}"
This works well for simple scenarios where each record can be cached independently. However, when dealing with collections or nested relationships, this can lead to many cache keys and unnecessary cache reads.
Using Parent Record's Updated Timestamp
A more efficient approach for related records is to use the parent record's updated_at
timestamp as a cache key for all its children. This strategy:
Reduces the number of cache keys to manage
Simplifies cache invalidation logic
Improves performance by reading fewer cache entries
Makes it easier to handle bulk updates
To implement this strategy:
Ensure child records touch their parent's timestamp
Create a cache key method on the parent
Use this key for caching collections of children
class Parent < ApplicationRecord
has_many :children
def children_cache_key
"#{model_name.to_s.downcase}/#{id}-#{updated_at}/children"
end
end
class Child < ApplicationRecord
belongs_to :parent, touch: true
end
Implementing Russian Doll Caching
Let's look at three practical examples that demonstrate both cache key strategies.
Example 1: Basic Blog Post with Comments
This example demonstrates the simplest form of Russian doll caching with the default Rails cache key strategy.
The post serves as the outer cache wrapper, with individual comment caches nested within.
When a comment is updated, its cache is invalidated, and the touch: true
option ensures the parent post's cache is also invalidated, maintaining cache consistency.
# app/models/post.rb
class Post < ApplicationRecord
has_many :comments
end
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :post, touch: true
end
# app/views/posts/show.html.erb
<% cache @post do %>
<article>
<h1><%= @post.title %></h1>
<div class="content">
<%= @post.content %>
</div>
<div class="comments">
<% @post.comments.each do |comment| %>
<% cache comment do %>
<div class="comment">
<p><%= comment.content %></p>
<span class="author"><%= comment.author_name %></span>
<span class="timestamp"><%= comment.created_at.strftime("%B %d, %Y") %></span>
</div>
<% end %>
<% end %>
</div>
</article>
<% end %>
Example 2: E-commerce Product Catalog with Parent Timestamp
This more complex example shows how to implement caching for a product catalog where categories contain multiple products, each with multiple variants.
This approach uses the parent timestamp strategy to efficiently cache collections and is particularly effective for e-commerce sites where:
Products frequently change (price, stock, etc.)
Categories contain many products
Products have multiple variants
Cache invalidation needs to happen in groups
# app/models/category.rb
class Category < ApplicationRecord
has_many :products
def products_cache_key
"category/#{id}-#{updated_at}/products"
end
end
# app/models/product.rb
class Product < ApplicationRecord
belongs_to :category, touch: true
has_many :variants
def variants_cache_key
"product/#{id}-#{updated_at}/variants"
end
end
# app/views/categories/show.html.erb
<% cache @category do %>
<div class="category">
<h1><%= @category.name %></h1>
<%# Cache all products using category's timestamp %>
<% cache @category.products_cache_key do %>
<div class="products-grid">
<% @category.products.includes(:variants).each do |product| %>
<div class="product-card">
<h3><%= product.name %></h3>
<%# Cache variants using product's timestamp %>
<% cache product.variants_cache_key do %>
<div class="variants">
<% product.variants.each do |variant| %>
<div class="variant">
<span class="size"><%= variant.size %></span>
<span class="price"><%= number_to_currency(variant.price) %></span>
<span class="stock"><%= variant.stock_level %> in stock</span>
</div>
<% end %>
</div>
<% end %>
</div>
<% end %>
</div>
<% end %>
</div>
<% end %>
Example 3: Social Media Feed with Mixed Caching Strategies
This advanced example demonstrates how to combine both caching strategies in a real-world social media scenario. It shows:
Using parent timestamp caching for the main feed of posts
Separate caching for relatively static user information
Custom cache keys for dynamic engagement data (likes and comments)
Handling user-specific content appropriately
This approach is ideal for social platforms where:
Content is frequently updated
User interactions happen constantly
Some data changes more frequently than others
Performance is critical for user experience
# app/models/feed.rb
class Feed < ApplicationRecord
belongs_to :user
has_many :posts
def posts_cache_key
"feed/#{user_id}-#{updated_at}/posts"
end
end
# app/models/post.rb
class Post < ApplicationRecord
belongs_to :feed, touch: true
has_many :comments
has_many :likes
def engagement_cache_key
"post/#{id}-#{updated_at}/engagement-#{likes.count}-#{comments.count}"
end
end
# app/views/feeds/show.html.erb
<% cache @feed do %>
<%# Cache all posts using feed's timestamp %>
<% cache @feed.posts_cache_key do %>
<div class="feed">
<% @feed.posts.includes(:user, :likes, :comments).each do |post| %>
<div class="post">
<%# Cache user info separately as it changes infrequently %>
<% cache post.user do %>
<div class="user-info">
<%= image_tag post.user.avatar_url, class: "avatar" %>
<span class="username"><%= post.user.username %></span>
</div>
<% end %>
<div class="content">
<%= post.content %>
</div>
<%# Cache engagement using custom key with counters %>
<% cache post.engagement_cache_key do %>
<div class="interactions">
<div class="likes">
<%= post.likes.count %> likes
</div>
<div class="comments">
<%= render partial: 'comments', collection: post.comments %>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
<% end %>
<% end %>
Best Practices and Considerations
Choosing the Right Cache Key Strategy
Use default Rails cache keys for independent records
Use parent timestamps for collections that change together
Consider custom cache keys for complex scenarios
Performance Optimization
Use
includes
to prevent N+1 queriesCache expensive computations
Monitor cache hit rates
Cache Invalidation
Use
touch: true
appropriatelyConsider using cache versioning for global changes
Implement proper cache clearing strategies
Common Pitfalls to Avoid
Don't cache user-specific content without proper keys
Be careful with time-based content
Watch out for memory usage with large cached fragments
Conclusion
Russian doll caching, combined with strategic cache key management, can significantly improve your Rails application's performance.
By understanding when to use default cache keys versus parent record timestamps, you can create efficient, scalable applications that provide excellent user experiences while minimizing server load.
Remember to always measure the impact of caching implementations and adjust your strategy based on your application's specific needs and usage patterns.
Proper monitoring and maintenance of your caching system are crucial for maintaining optimal performance.
Subscribe to my newsletter
Read articles from BestWeb Ventures directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
BestWeb Ventures
BestWeb Ventures
From cutting-edge web and app development, using Ruby on Rails, since 2005 to data-driven digital marketing strategies, we build, operate, and scale your MVP (Minimum Viable Product) for sustainable growth in today's competitive landscape.