Advanced Cache Techniques

View Source

This tutorial covers more advanced patterns and techniques for using ElixirCache in your applications.

Working with Cache TTL (Time To Live)

Setting appropriate TTL values is important for efficient cache usage:

# Cache for 10 seconds
MyApp.Cache.put("short-lived-key", :timer.seconds(10), %{data: "expires quickly"})

# Cache for 1 hour
MyApp.Cache.put("hourly-key", :timer.hours(1), %{data: "expires in an hour"})

# Cache for 1 day
MyApp.Cache.put("daily-key", :timer.hours(24), %{data: "expires in a day"})

# Cache indefinitely (no TTL)
MyApp.Cache.put("permanent-key", %{data: "never expires"})

Using Function Memoization

You can implement function memoization using ElixirCache:

defmodule MyApp.MemoizedCalculator do
  def factorial(n, cache \\ MyApp.Cache) do
    cache_key = "factorial:#{n}"
    
    case cache.get(cache_key) do
      {:ok, result} when not is_nil(result) ->
        result
        
      _ ->
        # Calculate the result if not in cache
        result = calculate_factorial(n)
        
        # Store in cache for future use (cache for 1 hour)
        cache.put(cache_key, 3600, result)
        
        result
    end
  end
  
  defp calculate_factorial(0), do: 1
  defp calculate_factorial(1), do: 1
  defp calculate_factorial(n) when n > 1, do: n * factorial(n - 1)
end

Implementing Cache Stampede Protection

Cache stampede occurs when many requests try to regenerate a cached item simultaneously:

defmodule MyApp.StampedeProtection do
  # Stale-while-revalidate pattern
  def get_with_protection(key, ttl, stale_ttl, generator_fn) do
    case MyApp.Cache.get(key) do
      # Cache hit - return value
      {:ok, %{value: value, timestamp: ts}} ->
        now = System.system_time(:second)
        
        # If stale but not expired, return stale value and trigger background refresh
        if now - ts > stale_ttl and now - ts < ttl do
          Task.start(fn -> 
            refresh_cache(key, ttl, generator_fn) 
          end)
        end
        
        {:ok, value}
        
      # Cache miss - generate and store
      _ ->
        refresh_cache(key, ttl, generator_fn)
    end
  end
  
  defp refresh_cache(key, ttl, generator_fn) do
    with {:ok, value} <- generator_fn.() do
      cached_value = %{
        value: value,
        timestamp: System.system_time(:millisecond)
      }
      
      MyApp.Cache.put(key, ttl, cached_value)
      {:ok, value}
    end
  end
end

Using Cache Prefixing for Namespaces

Organize your cache keys using prefixes:

defmodule MyApp.UserCache do
  @prefix "user:"
  
  def store_user(user_id, user_data) do
    MyApp.Cache.put("#{@prefix}#{user_id}", user_data)
  end
  
  def get_user(user_id) do
    MyApp.Cache.get("#{@prefix}#{user_id}")
  end
  
  def invalidate_user(user_id) do
    MyApp.Cache.delete("#{@prefix}#{user_id}")
  end
  
  # Bulk invalidation in ETS cache
  def invalidate_all_users() do
    if MyApp.Cache.cache_adapter() == Cache.ETS do
      MyApp.Cache.match_delete({:"#{@prefix}_", :_})
    else
      # Fallback for other adapters without pattern matching
      {:error, :not_supported}
    end
  end
end

Caching Database Queries

A common pattern is caching database query results:

defmodule MyApp.PostRepository do
  def get_featured_posts do
    cache_key = "featured_posts"
    
    case MyApp.Cache.get(cache_key) do
      {:ok, posts} when is_list(posts) ->
        {:ok, posts}
        
      _ ->
        # Query from database if not in cache
        with {:ok, posts} <- fetch_featured_posts_from_db() do
          # Cache for 15 minutes
          :ok = MyApp.Cache.put(cache_key, :timer.minutes(15), posts)
          {:ok, posts}
        end
    end
  end
  
  defp fetch_featured_posts_from_db do
    # Database query logic here
    # ...
  end
end

Working with Telemetry for Cache Monitoring

Implement telemetry handlers to monitor cache performance:

defmodule MyApp.CacheMonitor do
  def setup do
    :telemetry.attach(
      "cache-hit-rate-tracker",
      [:elixir_cache, :cache, :get],
      &handle_get_event/4,
      %{hits: 0, total: 0}
    )
    
    :telemetry.attach(
      "cache-miss-tracker",
      [:elixir_cache, :cache, :get, :miss],
      &handle_miss_event/4,
      %{misses: 0}
    )
  end
  
  def handle_get_event(_event, _measurements, _metadata, state) do
    total = state.total + 1
    
    if rem(total, 100) == 0 do
      hit_rate = state.hits / total
      IO.puts("Cache hit rate: #{hit_rate * 100}%")
    end
    
    %{state | total: total}
  end
  
  def handle_miss_event(_event, _measurements, _metadata, state) do
    misses = state.misses + 1
    %{state | misses: misses}
  end
end