R7.07 · Data Modeling & Memory — the chapter workshop
Three patterns, one dashboard.
The chapter capstone: which of R7’s six modeling patterns did codemojex actually reach for? The workshop folds the whole chapter through the game’s real dashboard read-models, each verified on disk, and names the ones the game left on the shelf.
Grounding: echo/apps/codemojex — Codemojex.Board (ZADD/ZREVRANGE cm:<game>:board), Codemojex.View.total_players/1 (SCARD cm:<game>:players), Codemojex.Rooms.add_player/2 (SADD), CodemojexWeb.RoomChannel (Phoenix Channels broadcast). Every surface verified on disk. Engine: Valkey. Keyspace: unbraced cm:<game>:… (not emq:{q}:).
R7 taught six modeling patterns; codemojex uses three. The other three are the roads not taken — domain and scale don’t call for them, and that gap is the chapter’s teaching.
The premise — six patterns, three in play
data-modeling · the chapterR7 taught six data-modeling patterns. A workshop is not a new pattern; it is the chapter’s techniques applied to one real consumer. The most useful question the assembly can ask is: which did codemojex actually reach for?
The answer is honest and narrow. codemojex builds three Valkey read-models for its dashboard. Each one applies a pattern from R7. The other three patterns are the honest absences — the game’s scale and domain don’t call for them. The chapter’s thesis closes on that gap.
# the six R7 patterns and codemojex's verdict R7.01 Redis as a primary database ✓ the bus’s job HASH (emq:{q}:job:<JOB>, noeviction + AOF) R7.02 Memory optimization ✓ the Board ZSET is compact, ordered, a clean read-model R7.03 Probabilistic data structures ✓ — exact SET for uniques (HLL is the road not taken) R7.04 Bitmaps ✗ no large-cardinality boolean flag; the cm-bitmapist spike is retired R7.05 Vectors ✗ no recommendation surface; no spike; domain doesn’t call for it R7.06 Geospatial ✗ a location-agnostic emoji game — nothing placed in space
The leaderboard — a compact sorted set
codemojex/board.ex · R7.02Codemojex.Board’s moduledoc states: “The leaderboard is one sorted set
per game … writes it straight to the board — no tier ladder, no first-mover bonus: the raw
linear score is the sole rank. Server-side, with no read-modify-write beyond the best-of fold.”
The key is cm:<game>:board — unbraced, unlike EchoMQ’s
emq:{q}:…. The <game> segment is a branded GAM id minted
at game start.
Two operations keep it current: record/3 writes a scored guess; top/2
reads the top-N for the dashboard or the leaderboard endpoint.
# key builder — UNBRACED: "cm:" <> game <> ":" <> suffix defp k(game, suffix), do: "cm:" <> game <> ":" <> suffix # record a scored guess (R7.02 — ZADD is one write, no lock) def record(game, player, base) do Cmd.zadd(k(game, "board")) |> Cmd.score(new_base, player) |> Wire.run(conn) end # top n, highest first: ZREVRANGE cm:<game>:board 0 n-1 WITHSCORES def top(game, n \\ 10) do Cmd.zrevrange(k(game, "board"), 0, n - 1) |> Cmd.withscores() |> Wire.run(Bus.conn()) end
The sorted set is the right read-model here: one ZADD updates the rank, one
ZREVRANGE … WITHSCORES reads the top-N for every leaderboard render. The ZSET
keeps no history and wastes no memory on old scores — only the player’s best stands.
It is exposed at GET /games/:id/leaderboard (the REST endpoint) and broadcast by
the room channel on every scored guess.
Notes on ValkeyZADD with an existing member
updates the score in place (no duplicate entry). ZREVRANGE … WITHSCORES returns
members and scores in descending order in a single round-trip. Both are core Valkey commands —
valkey.io/commands/zadd ·
valkey.io/commands/zrevrange.
The leaderboard is a sorted set: one write per guess, one read per render, no separate index to maintain. Compact, ordered, and single-key — the R7.02 discipline made concrete as a read-model.
Unique players — an exact set
codemojex/rooms.ex · codemojex/view.ex · R7.03A second Valkey key tracks which players joined a game: cm:<game>:players,
a plain SET. Codemojex.Rooms.add_player/2 adds each player on join:
# rooms.ex — SADD fills the players set on join
defp add_player(game, player),
do: Cmd.sadd("cm:" <> game <> ":players", player) |> Wire.run(Bus.conn())
Codemojex.View.total_players/1 counts it with SCARD:
# view.ex line 117 — SCARD returns the exact count
def total_players(game), do: scard("cm:" <> game <> ":players")
This is the R7.03 honest contrast. HyperLogLog is the pattern R7.03 taught for
large-cardinality uniques: 12 KB fixed memory, ~0.81% error, PFADD/PFCOUNT.
codemojex does not use it here. A game’s player count is small — an exact SET
costs a few bytes per player id, and the count is exact. HLL is the right move only when the
cardinality is so large that storing every id exactly is too expensive and a bounded error
rate is acceptable. This game’s scale doesn’t meet that bar.
State the trade both ways: exact SET at small cardinality — zero error, small memory. HyperLogLog at billion-scale — ~0.81% error, 12 KB regardless of cardinality. codemojex chooses the former; the pattern the chapter named is not discarded — it doesn’t fit the workload.
Notes on ValkeySADD key member [member ...]
adds members to a set; a member already present is silently a no-op. SCARD key returns
the exact cardinality in O(1). Both are core Valkey commands —
valkey.io/commands/sadd ·
valkey.io/commands/scard.
The exact SET at small cardinality is the right tool. HLL (R7.03) is not wrong — it is the right tool at a scale the game doesn’t have. The chapter teaches when each fits; this consumer makes the workload argument concrete.
The live view — a projection, not a stream
codemojex/view.ex · room_channel.ex · R5 road not takenCodemojex.View.game_view/1 folds the Valkey operational state — the board,
the player count, the privacy-gated game state — into the player-facing dashboard view.
CodemojexWeb.RoomChannel’s moduledoc states: “leaderboard updates
without any per-game process … a refresh re-reads the view and the
leaderboard.”
The room channel pushes the view to clients over Phoenix Channels whenever a guess is scored
(a :scored broadcast) or on a client refresh request. The channel joins
the game:<id> PubSub topic at join and receives every scored event for that game
without maintaining any per-game server process.
There is no XADD activity stream in codemojex. This is the R5
road not taken: an event-sourced activity feed (XADD to an append-only log, consumers
reading from an offset) is the R5 pattern. codemojex pushes a re-read Valkey projection over
Phoenix Channels instead — simpler, sufficient for the game’s real-time needs,
no log to maintain.
# the channel join — game_view/1 is a Valkey read, not a stream replay def join("game:" <> game, _params, socket) do {:ok, %{view: Codemojex.game_view(game)}, assign(socket, :game, game)} end # a scored guess arrives from the PubSub topic — push to the client def handle_info({:scored, payload}, socket) do push(socket, "scored", payload) {:noreply, socket} end # a client refresh re-reads the Valkey view + the leaderboard; no log seek def handle_in("refresh", _params, socket) do game = socket.assigns.game reply = %{view: Codemojex.game_view(game), leaderboard: Enum.map(Codemojex.leaderboard(game, 20), ...)} {:reply, {:ok, reply}, socket} end
The live view is a projection, not a log replay. Phoenix Channels carry the update;
a client refresh re-reads from Valkey. No XADD; no consumer offset; no
stream lag. The simpler real choice.
The honest subset — the chapter thesis
the six patterns · three used · three not takenThe chapter’s instruction was “know which pattern fits which workload.” The workshop makes it concrete. codemojex uses three of R7’s six patterns. The other three are absent because the game’s scale and domain don’t reach for them — not because they are bad patterns. Every absent pattern is the right tool for a different workload.
Static verdicts — job HASH ✓ · Board ZSET ✓ · exact SET ✓ / HLL ✗ small scale · bitmaps ✗ no dense flag · vectors ✗ no recommender · geo ✗ no location dimension.
Six data-modeling patterns: Redis as a primary database (R7.01), compact encodings (R7.02), probabilistic structures (R7.03), bitmaps (R7.04), vector sets (R7.05), geospatial (R7.06). Each trades memory, accuracy, or speed for a different workload shape.
Codemojex.Board — ZADD/ZREVRANGE
cm:<game>:board (R7.02 compact ZSET). View.total_players/1 —
SCARD cm:<game>:players filled by Rooms.add_player —
SADD (R7.03 exact SET, small scale). The live view is a Valkey projection pushed
by CodemojexWeb.RoomChannel — no XADD stream.
A real system uses a small, workload-chosen subset of the modeling family. codemojex reaches for three patterns; the other three are absent because the scale and domain don’t call for them. “Know which pattern fits which workload” — made concrete by what one consumer built and left on the shelf.
Redis as a primary database
The job HASH as the record of truth: emq:{q}:job:<JOB>, noeviction,
AOF everysec. The bus’s R7.01 applied form.
Memory optimization
Compact encodings, short fields, capped structures. The Board ZSET applies these: minimal fields, one write per guess, one read per render.
R7.03 · in this courseProbabilistic data structures
HLL vs exact SET: when the cardinality is small, an exact SET is the right call. The contrast that R7.03 set up, landed here.
R7.04 · in this courseBitmaps
Boolean flags at 1 bit per entity. codemojex has no live bitmap surface; a retired analytics spike was designed to use the player’s branded-id placement hash as the bit offset.
R7.05 · in this courseVector sets
HNSW similarity search. No recommendation surface in codemojex today and no infra spike.
R7.06 · in this courseGeospatial
GEOADD/GEOSEARCH over a geohash sorted set. A location-agnostic emoji game has no location dimension to model.
/bcs · B7 · codemojexThe codemojex manuscript chapter
The worked consumer as the BCS manuscript tells it: the game on the BCS stack, its entities branded, its guesses jobs on per-player lanes, its competitive state in Valkey. The workshop’s read-models are the manuscript’s chapter on the operational data layer.
/bcs/overviewBCS overview — B0
The branded-id contract and the identity that ties the job HASH, the Board ZSET, and the
player SET together: one GAM id, one PLR id, every key.
Data Modeling & Memory
The chapter that the workshop closes: six patterns, the modeling family, the thesis made concrete.
References
sources · relatedSources
- Valkey — ZADD — add or update a member with a score in a sorted set; the write half of the Board ZSET.
- Valkey — ZREVRANGE — return a range in descending order with optional scores; the leaderboard read.
- Valkey — SADD — add members to a set; no-op if already present; fills
cm:<game>:playerson join. - Valkey — SCARD — return the exact cardinality of a set in O(1);
View.total_players/1.
Related in this course
- R7 · Data Modeling & Memorythe chapter the workshop closes
- R7.01 · Redis as a primary databasethe job HASH as the record of truth
- R7.02 · Memory optimizationcompact encodings and capped structures
- R7.03 · Probabilistic data structuresHLL vs exact SET — the contrast this page lands
- R7.04 · Bitmapsboolean flags at 1 bit per entity
- R7.05 · Vectors & similarity searchHNSW; the absent road
- R7.06 · Geospatialgeohash sorted set; the absent road
- BCS · B7 codemojexthe manuscript chapter for the consumer
- BCS · overviewthe branded-id contract that ties the keys