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 timestamp

  • Any 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:

  1. Ensure child records touch their parent's timestamp

  2. Create a cache key method on the parent

  3. 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

  1. 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

  2. Performance Optimization

    • Use includes to prevent N+1 queries

    • Cache expensive computations

    • Monitor cache hit rates

  3. Cache Invalidation

    • Use touch: true appropriately

    • Consider using cache versioning for global changes

    • Implement proper cache clearing strategies

  4. 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.

0
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.