jonnifyelixir

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

Portal.Store %{ns => champ} USR users USR0NbAb1xcFCy 1 LSN lessons empty CHAMP 0 PGE pages empty CHAMP 0 prefix routes each id to its lane

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

put USR0NbAb1xcFCy 1 · validate well-formed USR id 2 · route by namespace USR → users partition 3 · store in partition CHAMP users CHAMP · 3 entries · 1 level 4 · Snowflake → created_at created 2026-05-31 13:35:19 UTC

      

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.

F4.12.1 · put & partition
Watch a branded CHAMP grow
Insert branded keys one by one and watch the store take shape — each key routed to its partition by namespace, each partition’s CHAMP filling, new partitions appearing on first use.
F4.12.2 · get & resolve
A Snowflake registry
Resolve any branded id in one call: route it to its partition, look it up in O(log₃₂ n) hops, and read its creation time straight out of the Snowflake — or reject it if the namespace is wrong.
F4.12.3 · range by time
Query by time range
Because Snowflake ids are time-ordered, a time window becomes an id range: compute the bounds, scan one partition, and return the entries created inside it — no separate timestamp column.

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

Related in this course