How to Choose the Right Cache Adapter

View Source

ElixirCache supports multiple cache adapters, each with its own strengths and use cases. This guide will help you choose the most appropriate adapter for your specific needs.

Available Adapters

ElixirCache provides the following adapters:

  1. Cache.ETS - Erlang Term Storage
  2. Cache.DETS - Disk-based ETS
  3. Cache.Redis - Redis-backed distributed cache
  4. Cache.Agent - Simple Agent-based in-memory cache
  5. Cache.ConCache - ConCache wrapper
  6. Cache.PersistentTerm - Erlang :persistent_term for rarely-written values
  7. Cache.Counter - Lock-free atomic integer counters
  8. Cache.Sandbox - Isolated cache for testing

Choosing an Adapter

Cache.ETS

Best for:

  • High-performance in-memory caching
  • Single node applications
  • Low-latency requirements

Configuration example:

defmodule MyApp.Cache do
  use Cache,
    adapter: Cache.ETS,
    name: :my_app_cache,
    opts: [
      read_concurrency: true,
      write_concurrency: true
    ]
end

Cache.DETS

Best for:

  • Persistent caching across application restarts
  • Larger datasets that shouldn't be lost on restart
  • Less frequent access patterns

Configuration example:

defmodule MyApp.PersistentCache do
  use Cache,
    adapter: Cache.DETS,
    name: :my_app_persistent_cache,
    opts: [
      file_path: "/tmp/cache_data"
    ]
end

Cache.Redis

Best for:

  • Distributed applications running on multiple nodes
  • Systems requiring shared cache across services
  • Applications needing advanced features like expiration, pub/sub, etc.

Configuration example:

defmodule MyApp.DistributedCache do
  use Cache,
    adapter: Cache.Redis,
    name: :my_app_redis_cache,
    opts: [
      uri: "redis://localhost:6379",
      size: 10,
      max_overflow: 5
    ]
end

Cache.Agent

Best for:

  • Simple use cases
  • Small applications
  • Development environments

Configuration example:

defmodule MyApp.SimpleCache do
  use Cache,
    adapter: Cache.Agent,
    name: :my_app_simple_cache
end

Cache.ConCache

Best for:

  • Applications already using ConCache
  • Needs for automatic key expiration and callback execution

Configuration example:

defmodule MyApp.ConCache do
  use Cache,
    adapter: Cache.ConCache,
    name: :my_app_con_cache,
    opts: [
      ttl_check_interval: :timer.seconds(1),
      global_ttl: :timer.minutes(10)
    ]
end

Cache.PersistentTerm

Best for:

  • Configuration data or look-up tables that change infrequently
  • Extremely high-read, low-write workloads
  • Single-node or multi-node scenarios where write cost is acceptable

Backed by Erlang's :persistent_term storage. Reads are constant-time and require no locking or process round-trips, making them faster than ETS for read-heavy workloads. However, every write or delete copies the entire persistent term table, so this adapter is not suitable for frequently mutated keys.

Note: TTL is not supported — values persist until explicitly deleted.

Configuration example:

defmodule MyApp.ConfigCache do
  use Cache,
    adapter: Cache.PersistentTerm,
    name: :my_app_config_cache,
    opts: []
end

Cache.Counter

Best for:

  • Atomic integer counters (page views, rate limiting, event counts)
  • High-concurrency write workloads where lock-free semantics matter

Backed by Erlang's :counters module, which provides lock-free atomic increment and decrement. Counter references and the key-to-index mapping are stored in :persistent_term so all processes can access them without a process round-trip.

Using use Cache with this adapter injects increment/1,2 and decrement/1,2 into your module in addition to the standard get/1, put/3, and delete/1 functions.

Note: put/3 only accepts 1 or -1 as values (increment/decrement). Use increment/1,2 and decrement/1,2 for more ergonomic access.

Configuration example:

defmodule MyApp.CounterCache do
  use Cache,
    adapter: Cache.Counter,
    name: :my_app_counter_cache,
    opts: [initial_size: 32]
end

MyApp.CounterCache.increment(:page_views)
MyApp.CounterCache.decrement(:active_users)
{:ok, count} = MyApp.CounterCache.get(:page_views)

Cache.Sandbox

Best for:

  • Testing environments
  • Isolated tests that shouldn't interfere with each other

Configuration example:

defmodule MyApp.TestCache do
  use Cache,
    adapter: Cache.ETS,
    name: :my_app_test_cache,
    sandbox?: true
end

Switching Between Adapters

One of the main benefits of ElixirCache is the ability to easily switch between adapters without changing your application code. You can use different adapters in different environments:

defmodule MyApp.Cache do
  use Cache,
    adapter: get_adapter(),
    name: :my_app_cache,
    opts: get_opts()

  if Mix.env() === :test do
    defp get_adapter, do: Cache.ETS
    defp get_opts, do: []
  else
    defp get_adapter, do: Cache.Redis
    defp get_opts, do: [uri: "redis://localhost:6379", size: 10]
  end
end

Performance Considerations

When choosing an adapter, consider:

  1. Access patterns - How frequently are you reading vs writing?
  2. Data volume - How much data will be stored?
  3. Persistence requirements - Does the data need to survive restarts?
  4. Distribution needs - Will multiple nodes/services need access?
  5. Complexity - Do you need advanced features or simple key-value storage?

Always benchmark different options with your specific workload to determine the best fit.