ExpirableStore
Lightweight expirable value store for Elixir with cluster-wide or local scoping.
Perfect for caching OAuth tokens, API keys, and other time-sensitive data that shouldn't be repeatedly refreshed.
Features
- Smart caching: Caches successful results, retries on failure
- Flexible scoping: Cluster-wide replication or node-local storage
- Refresh strategies: Lazy (on-demand) or eager (background pre-refresh)
- Keyed expirables: Same logic, independent cache and timer per runtime key
- Concurrency-safe: Concurrent access protected by
:global.trans/2 - Clean DSL: Spark-based compile-time configuration
- Named functions: Auto-generated functions for each expirable
Installation
def deps do
[{:expirable_store, "~> 0.5.0"}]
endQuick Example
defmodule MyApp.Expirables do
use ExpirableStore
# Cluster-scoped, lazy refresh
expirable :github_access_token do
fetch fn -> GitHubOAuth.fetch_access_token() end
scope :cluster
refresh :lazy
end
# Node-local, eager refresh (30s before expiry)
expirable :datadog_agent_token do
fetch fn -> DatadogAgent.fetch_local_token() end
scope :local
refresh {:eager, before_expiry: :timer.seconds(30)}
end
# Never expires (cached until explicitly cleared)
expirable :static_config do
fetch fn -> {:ok, load_config(), :infinity} end
end
# Keyed: independent cache and timer per GitHub App installation
expirable :github_installation_token do
fetch fn installation_id ->
{:ok, GitHubApp.get_installation_token(installation_id), System.system_time(:millisecond) + :timer.hours(1)}
end
keyed true
scope :cluster
refresh {:eager, before_expiry: :timer.minutes(5)}
end
end# Non-keyed: named functions (recommended)
{:ok, token, expires_at} = MyApp.Expirables.github_access_token()
token = MyApp.Expirables.github_access_token!()
# Non-keyed: generic functions
{:ok, token, _} = MyApp.Expirables.fetch(:github_access_token)
MyApp.Expirables.clear(:github_access_token)
MyApp.Expirables.clear_all()
# Keyed: pass a runtime key
{:ok, token, _} = MyApp.Expirables.github_installation_token(123)
token = MyApp.Expirables.github_installation_token!(123)
MyApp.Expirables.clear(:github_installation_token, 123) # clear specific key
MyApp.Expirables.clear(:github_installation_token) # clear all keysDSL Options
| Option | Values | Default | Description |
|---|---|---|---|
fetch | fn -> {:ok, value, expires_at} | :error end | required |
Fetch function. Use 1-arity when keyed: true. expires_at is Unix timestamp in ms or :infinity |
keyed | true, false | false |
When true, each unique key gets its own independent cache entry and timer |
refresh | :lazy, {:eager, before_expiry: ms} | :lazy | Refresh strategy |
scope | :cluster, :local | :cluster | Scope of the store |
Refresh Strategies
:lazy(default): Refresh on next fetch after expiry{:eager, before_expiry: ms}: Background refreshmsmilliseconds before expiry. Does not apply to:infinityvalues
Scope Options
:cluster(default): Each node maintains a local Agent copy; updates coordinated via:global.trans/2and replicated via:pg:local: Node-local Agent, no cross-node replication
Keyed Expirables
When keyed: true, the fetch function receives the key as its argument. Each unique key has its own independent Agent and refresh timer. The key can be any Erlang term:
MyApp.Expirables.github_installation_token(123) # integer
MyApp.Expirables.github_installation_token(:tenant_a) # atom
MyApp.Expirables.github_installation_token({:org, 42}) # tupleGenerated Functions
For expirable :name (keyed: false):
name()/name!()— fetch value or raise
For expirable :name (keyed: true):
name(key)/name!(key)— fetch value for key or raise
Generic functions (always available):
fetch(name)/fetch(name, key)fetch!(name)/fetch!(name, key)clear(name)/clear(name, key)clear_all()
Key Behaviors
:errorresults are never cached — fetch is retried on every callexpires_atmust be a Unix timestamp in milliseconds or:infinity- For keyed expirables, the number of keys need not be known at compile time
When to Use
Good for lightweight, time-sensitive data: OAuth tokens, API keys, FX rates, per-tenant credentials.
Not recommended for: high-traffic scenarios, large values.
Development
mix test # single-node tests
mix test.all # includes distributed multi-node tests