For application developers who need a coherent distributed cache. Assumes the overview and ownership.
Fief.Cache is the other first-class surface on Fief's ownership substrate,
co-equal with Fief.Key and sharing its Fief.Hasher
key→vnode seam: a partitioned, single-owner cache with a plain
get/put/delete API. Each key lives on exactly one node at a time, so the
coordination machinery buys you single-writer-per-key coherence rather
than the process lifecycles of Fief.Key. An instance runs exactly one
surface; to run a cache and a key layer, run two instances.
When to reach for it
Three tools sit near each other here, and the honest positioning matters more than the feature list:
- vs a plain AP cache (an ETS cache,
nebulex, a Redis you treat as best-effort): reach forFief.Cacheonly when you need at most one writer per key across the cluster — when two nodes writing the same key behind each other's backs is a correctness bug, not a nuisance. If occasionally-stale entries are fine — and for most caching they are — a plain AP cache is the right call: it is simpler and stays available through partitions, whereasFief.Cachedeliberately goes unavailable rather than let two owners serve one key. (Advice; this is the same register as the overview's "when not to use it".) - vs
Fief.Key: reach forFief.Cachewhen you need lookup, not lifecycle — a value keyed by a term, with no callbacks and no state that has to survive an ownership transfer.Fief.Keyis the right surface when a key is a living thing: per-key state that must follow ownership, timers and side effects that must fire on exactly one node, a serialize/rehydrate handover. A cache entry has none of that — it is disposable by definition, which is exactly why the surface can be this small.
Quickstart
A cache is an ordinary Fief instance whose vnode_impl is
Fief.Cache.VnodeImpl — the trivial reference implementation of the
Fief.Vnode behaviour that backs each vnode with
one ETS table. Everything else — the authority, the partition count, the lease
TTL — is configured exactly as for any instance
(getting started).
# in MyApp.Application.start/2
children = [
MyApp.Repo,
{Fief,
name: MyApp.Cache,
authority: {Fief.Authority.Postgres, repo: MyApp.Repo},
vnode_impl: {Fief.Cache.VnodeImpl, []},
partitions: 1024,
lease_ttl: 5_000},
# ... the rest of your tree
]There is no use Fief.Cache and no cache module to define: the instance is
the cache. Call it directly:
iex> Fief.Cache.put(MyApp.Cache, "user:42", %{name: "Ada"})
:ok
iex> Fief.Cache.get(MyApp.Cache, "user:42")
{:ok, %{name: "Ada"}}
iex> Fief.Cache.delete(MyApp.Cache, "user:42")
:ok
iex> Fief.Cache.get(MyApp.Cache, "user:42")
:errorThis snippet is executed by test/guides/cache_test.exs, which boots an
instance with this configuration and drives this exact round-trip; where the
test deviates (its repo, instance name, partition count, and tightened timing
intervals) the deviation is named in that file's moduledoc. The round-trip is
grounded further by test/fief/cache_test.exs —
describe "put/get/delete round-trip through the real router".
Semantics
Partitioned single-owner, not replicated
A key hashes to one vnode, and that vnode lives on exactly one node. There is
no cross-node replication and no local read-through copy, so a get for a
key owned by another node is a real network round trip through the router,
not a local read — by design. The upside of paying that hop is the property in
the next section; the trade is deliberate, and cross-node routing is grounded
by test/fief/cache_test.exs —
describe "put/get/delete round-trip through the real router" (the non-owner
routing test, where a client on a non-owning node still round-trips).
Single-writer-per-key coherence
Because at most one node ever owns a given key at a time — Fief's
single-ownership guarantee — a put and a
concurrent get for the same key can never race across two nodes that both
believe they own it. That is what a coherent cache buys over an AP one:
read-through caching without cross-node stale-write races. This is the routing
half of guarantee 1, which is unconditional, and it is what a future
loader-backed fetch (below) will build dogpile prevention on.
Drop-on-handoff
Any ownership move — a rebalance, a node death, a join or a leave — empties the
moved vnode's entries. Fief.Cache.VnodeImpl reports drained immediately on
handoff_out (a cache holds nothing worth shipping) and starts empty on
handoff_in. So failover means a cold cache: the new owner starts with
misses and repopulates, never with stale entries and never serving alongside
the old owner. This is grounded by test/fief/cache_test.exs —
describe "drop-on-handoff (entries do not survive an ownership transfer)",
which asserts the new owner starts empty after the vnode moves.
No durability, by definition
Entries have no durability of any kind — not across an ownership move, not
across a process restart, not across a node death. This is a cache: the
durable source of truth lives elsewhere, and that is precisely what lets the
surface drop entries freely. For Fief.Cache, guarantee 1's state layer is
unconditional — entries live only in the table owned by the vnode's serving
process and die with it, so there is no fencing policy to weaken
(guarantee 1, per-surface scope).
Unavailability windows
A cache key can be unavailable by design for a bounded period — mid-transfer
with the donor unreachable, or with the arbiter down — for exactly the same
reasons and durations as any Fief surface. Rather than re-enumerate them, see
the netsplit matrix for every partition case
and delivery and errors for
what a caller observes. With a cache the cost of these windows is smaller than
with Fief.Key: unavailability degrades to misses against your durable store,
not lost state.
API and error shapes
The API is Map.fetch-shaped, wrapping Fief.Router.call:
| Call | Success | Miss | Routing failure |
|---|---|---|---|
get(instance, key) | {:ok, value} | :error | {:error, reason} |
put(instance, key, value) | :ok | — | {:error, reason} |
delete(instance, key) | :ok | — | {:error, reason} |
put and delete return :ok (a delete is :ok whether or not the key was
present); get returns {:ok, value} on a hit and :error on a miss —
distinct from {:error, reason}, which is a routing failure, not a miss.
owner_of(instance, key) returns the hint-grade presumed owner, exactly like
Fief.Router.owner_of.
As with every Fief surface, {:error, :timeout} means unknown outcome —
the put may or may not have been applied — while every other error reason is
safe to retry blindly. The reason atoms (:timeout, :exhausted,
:no_owner, :noconnect, :unreachable, :not_running) and their
retry-safety are documented once, for callers, on
delivery and errors — the same
table applies here unchanged, so this page cross-links rather than duplicates
it.
Structural misuse raises rather than returning an error: a non-binary key the
instance's hasher rejects, or an instance with no vnode impl, is a caller bug
caught at the call site, not a runtime condition. Those raises are grounded by
test/fief/cache_test.exs — describe "Fief.Cache.vnode!/2 structural raises".
Configuration deltas
A cache instance takes the same options as any instance; only two points are cache-flavored:
hasherfor non-binary keys. Keys are binaries by default. To key the cache on any other term, pass a customhasher— a module implementingFief.Hasher, never a fun, because it is fingerprinted (below). It is passed inside the impl opts:vnode_impl: {Fief.Cache.VnodeImpl, hasher: MyApp.Hasher}.- The join-time fingerprint gate. Because
Fief.Cachehashes keys to vnodes caller-side, every node must agree on thehashermodule, or a joiner would compute a different vnode for the same key and silently split the keyspace. Fief forecloses that: the fingerprint is checked at join, before the joiner touches shared state, and a mismatch is fatal — the node refuses to join. This is grounded bytest/fief/cache_test.exs—describe "join fingerprint (cache-flavored; mechanism proved generically elsewhere)".
partitions is immutable per instance, as for any surface: the keyspace split
is fixed at the first boot of the namespace and every later joiner is checked
against it. Repartitioning is a second instance, run blue/green — see
the instance model.
Roadmap
(This section is a roadmap, not a present-tense promise — v1 ships
get/put/delete only.)
Three fast-follows are planned, and the single-owner placement is what makes each sound:
- TTL and eviction — per-entry expiry and size bounds, armed inside
Fief.Cache.VnodeImplon the same timer seamFief.Keyuses for its sweep cadence. fetch/4with a loader — read-through with a caller-supplied loader, run in an implementation-owned process so a slow loader never blocks the serving agent. Because only one node owns a key's vnode at a time, only one loader per key can ever be in flight — dogpile prevention falls out of the single-owner property rather than needing a separate lock.- Warm handoff — shipping entries to the new owner on a planned transfer (a rebalance, a graceful leave) instead of dropping them, so only unplanned moves cost a cold cache. A node death stays cold no matter what: there is no donor left to ship from, and drop-on-handoff remains the recovery semantics.
Design notes
docs/implementation.md§6.1 — the trivial cache implementation and its place in the vnode layer.docs/design.md§5 — the planner's single-owner exclusivity that single-writer coherence rests on.docs/design.md§9 — the immutable hash and partition count behind the fingerprint gate.