F4 · Lab · module 12
Lab: build a branded CHAMP store
This chapter built the parts in isolation — branded ids that validate and decode
(F4.07–F4.08), the CHAMP node that holds them cheaply (F4.06), the partitioned store behind a GenServer
(F4.09), the recipes that read it (F4.10). The lab assembles them into one Portal.Store
you can keep as a reference. A put is validated, routed to a partition by its
namespace, stored in that partition’s CHAMP, and its Snowflake gives the creation time at no extra cost.
Three dives build it, register entities, and query by time.
Start with the whole stack at once: trace a single entity from put to
stored, through the four layers the dives assemble. Pick an entity.
The store · namespace → CHAMP
store: USR→1, LSN→0, PGE→0
put routes by the 3-letter prefix; get and range read one lane.
Trace the stack
Every write passes through the same four layers, each one a piece built earlier in the chapter. The id is checked, its three-letter namespace routes it to a partition, the entry lands in that partition’s CHAMP, and the Snowflake embedded in the id is its timestamp. Pick an entity to trace.
Entity · select one
partition: users
created: 2026-05-31 13:35:19
One id carries everything the store needs: it validates itself, names its own partition, and dates itself. The four layers are the four earlier lessons, stacked into one write path.
Build it · three dives
Each dive assembles and exercises one capability of the store, with the code to keep.
Advanced: the assembled module
The whole store is a GenServer holding one map from namespace to that namespace’s CHAMP —
%{"USR" => champ, "LSN" => champ, ...}. A write routes on the three-letter
prefix and updates only that partition; because the partition is an immutable CHAMP, the update returns a new
map that shares everything it did not touch, so a snapshot of the whole store is a single cheap reference. The
GenServer serialises writes through its mailbox while reads run lock-free against the last published map —
the split from F4.09. Nothing here is new; it is the chapter’s parts named once and wired together.
The three operations below are the public surface. put/2 validates and routes;
get/1 resolves an id to its entry and its embedded timestamp;
range/2 turns a time window into id bounds and scans a partition. The dives build
each one in turn.
# the assembled store — a GenServer over %{namespace => CHAMP} defmodule Portal.Store do use GenServer def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__) def put(id, value), do: GenServer.call(__MODULE__, {:put, id, value}) # write — serialised def get(id), do: GenServer.call(__MODULE__, {:get, id}) # resolve by id def range(ns, {from, to}), do: GenServer.call(__MODULE__, {:range, ns, from, to}) def init(state), do: {:ok, state} # state = %{ns => champ}; built by the dives end
the parts (F4.05–F4.11)
HAMT, CHAMP, branded ids, persistence, the store, the recipes, the algorithms.
one Portal.Store
A partitioned, branded-CHAMP, GenServer-owned store you can read as a reference.
Build it across watch a branded CHAMP grow, a Snowflake registry, and query by time range. Predecessor: F4.11 — Dynamic programming & advanced problems. This is the final module of the chapter.
References
Primary sources for this lab, and the lessons it assembles.
Sources
- Elixir — GenServer — the process that owns the store.
- Elixir —
update_in/3— updating one partition in the map. - Elixir — Map — the namespace-to-CHAMP map at the top.
Related in this course
- F4.09 · Branded CHAMP maps & GenServer — the store this lab builds.
- F4.06 · CHAMP — the node inside each partition.
- F4.08 · Branded ids & persistence — validation and time-range queries.
- F4 · Algorithms & Data Structures