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 for Fief.Cache only 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, whereas Fief.Cache deliberately 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 for Fief.Cache when 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.Key is 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")
:error

This 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.exsdescribe "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.exsdescribe "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.exsdescribe "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:

CallSuccessMissRouting 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.exsdescribe "Fief.Cache.vnode!/2 structural raises".

Configuration deltas

A cache instance takes the same options as any instance; only two points are cache-flavored:

  • hasher for non-binary keys. Keys are binaries by default. To key the cache on any other term, pass a custom hasher — a module implementing Fief.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.Cache hashes keys to vnodes caller-side, every node must agree on the hasher module, 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 by test/fief/cache_test.exsdescribe "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.VnodeImpl on the same timer seam Fief.Key uses for its sweep cadence.
  • fetch/4 with 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.