Caching using ets

When using external services (usually via an api call) I sometimes want to cache the results for a period of time. Here’s a technique that works for me

In this example I have a cached version of a lookup_function called lookup_function_cached that takes a key and returns a result

Initialise the cache

The module will need to be initialised at startup to create the ets table

defmodule AwesomeApp.Lookup do
  def init() do
    # I've named the table the same as the module
    :ets.new(AwesomeApp.Lookup, [:named_table, :public, read_concurrency: true])
  end
  
  def lookup_function(key) do
    :timer.sleep(3000)
    "3 seconds can feel like a lifetime"
  end
end

Create the lookup function and check the cache has not expired (24 hours)

  @ttl 24 * 60 * 60

  defp lookup_function_cache_find(key) do
    case :ets.lookup(AwesomeApp.Lookup, key) do
      [result | _] ->
        lookup_function_cache_check(result)
      [] ->
        nil
    end
  end

  defp lookup_function_cache_check({_, result, expiration}) do
    cond do
      expiration > :os.system_time(:seconds) ->
        result
      true ->
        nil
    end
  end

Look in the cache or save

  def lookup_function_cached(key) do
    case lookup_function_cache_find(hostname) do
      {:ok, result} ->
        {:ok, result}

      _ ->
          key
        |> lookup_function()
        |> lookup_function_cache_save(key)
    end
  end

  defp lookup_function_cache_save(result, key) do
    expiration = :os.system_time(:seconds) + @ttl
    :ets.insert(AwesomeApp.Lookup, {key, result, expiration})
    result
  end