Codemojex · dive C0.3
The architecture — four layers, one glance.
Codemojex is four layers, and the layering is the architecture. Requests enter at a thin
Phoenix surface behind privacy-safe views; the Codemojex facade delegates into the
domain systems; durable work travels on the EchoMQ bus over Valkey, on lanes named by branded ids; and
state rests on a three-tier storage floor — Postgres the system of record, Valkey the derived
real-time state, EchoStore the immutable near-cache. Money and the secret live on the floor. One glance holds
the whole game.
Hover or select a layer. The readout names what the layer owns and the real modules that own it.
lib/codemojex/game.ex and the design
§The architecture at a glance.The surface — codemojex_web/*: the JSON API,
the room channel, and the LiveView lobby, all reading through Codemojex.View — no secret, no
others' guesses. The surface holds no game state and never scores.
One sentence holds the game
game.ex · the facade moduledocThe facade module's own documentation is the architecture in one sentence. Read it once as prose, then once as a map — every clause is a layer of the stack above.
Codemojex on the bus: a six-emoji code-breaking competition whose entities are branded components persisted in Postgres, whose guesses are jobs on per-player lanes scored by a single authority, whose three currencies mutate atomically in the database through a wallet with a transaction ledger, whose rooms template their games, and whose diamond prize pools settle through a second queue (winner-take-all for a classic game, a sealed top-K split for a golden game).
Codemojex moduledoc, lib/codemojex/game.ex — quoted verbatim.“Branded components persisted in Postgres” is the floor.
“Jobs on per-player lanes scored by a single authority” is the bus and its consumers. “A wallet
with a transaction ledger” is a domain system behind the facade. And the moduledoc closes where the page
ends: “Startup is Codemojex.Application's job — the Repo, PubSub, the EchoMQ bus and
consumers, and the Phoenix endpoint come up there.”
Layer by layer
surface → facade → bus → floorLayer 1 — the surface. codemojex_web/*: the JSON
API, the room channel, and the LiveView lobby. Every read goes through Codemojex.View, whose moduledoc
opens with the invariant: “nothing here returns the secret, and nothing returns another player's
guesses.” The facade marks its view delegates the same way — “views (privacy-preserving: no
secret, no others' guesses).”
Layer 2 — the facade and the systems. The
Codemojex module is a thin facade delegating into small domain systems, each owning one concern:
Codemojex.Guesses (the play API), Codemojex.Rooms (templates and lifecycle),
Codemojex.Wallet (the currencies, atomically in Postgres), Codemojex.Scoring (the pure
linear engine), Codemojex.Board (the competitive state), Codemojex.Settle (the payout
job). Codemojex.Guesses.submit/3 validates a six-emoji guess against the game's keyboard, overlays the
player's locks (Codemojex.Locks.merge/3), charges the room's currency through the wallet — and
then hands the guess off. The host never scores; the consumer does.
Layer 3 — the bus. submit/3 mints a branded
JOB (EchoData.BrandedId.generate!("JOB")) and enqueues it —
Lanes.enqueue(Bus.conn(), @queue, player, job, payload), queue "cm". The lane is named by
the player's PLR, “so the bus rotates service across players and one keyboard masher cannot
starve the field.” Codemojex.ScoreWorker is the single scoring authority:
EchoMQ.Consumer drains the queue through Lanes.claim, the player id arriving as the lane
group. Settlement is a second queue, "cm-settle", whose lane is the game's GAM —
“the move-then-settle split is the Exchange pattern: the guess queue competes, the settle queue pays”
(Codemojex.Settle). Two more lanes complete the picture: notifications on "cm.notify" and
inbound bot commands, each keyed by chat and drained by its own consumer.
Layer 4 — the floor. Three tiers, each holding the data
that fits it. Postgres is the system of record — Codemojex.Store.game/1 reads it, and players,
transactions, rooms, games (secret included, server-side), emoji sets, and guesses rest there. Valkey holds the
derived real-time state — the per-game leaderboard sorted set Codemojex.Board writes, the attempt
counters, and the bus itself. EchoStore is the immutable near-cache — Codemojex.Cache.fetch_game/1
over the tables Codemojex.Tables declares (:cm_games, :cm_emojisets), an L1
:ets hit in the caller's process with the shared Valkey as L2. A game's secret is immutable for the
game's life, so coherence is :none and the cache never goes stale.
The tree stands the floor up first. Codemojex.Application starts the
layers in dependency order: “the relational system of record (Repo) and PubSub
first; then the EchoMQ bus (Bus, the shared Valkey connector); then the EchoStore near-cache tier
(Tables …); then the consumers — the scoring authority, the settlement worker, the
notification worker, and the bot-command worker …; then the Phoenix endpoint.”
Grounding — game.ex (the facade, Guesses, ScoreWorker,
Settle) · store.ex (Store, Bus, Cache) ·
board.ex · view.ex · application.ex · the design §The
architecture at a glance.
A guess's journey
submit → scoredOne entity crossing all four layers, top to floor and back. Step the six
stations: a guess leaves the facade as a JOB on the player's lane, is claimed by the single scoring
authority, touches every storage tier, and returns to the surface as a scored event.
Codemojex.Guesses and
Codemojex.ScoreWorker, lib/codemojex/game.ex.Submit — Codemojex.Guesses.submit(game, player,
emojis) reads the game's mutable state from the system of record (Store.game/1), validates the
six emoji against the keyboard, overlays locks (Locks.merge/3), and charges the wallet
(Wallet.charge_guess/3). Layer 2 work — the host never scores.
The path is asymmetric on purpose. The submit side runs on the caller and
touches only the system of record and the wallet; the scoring side runs on the consumer and is the only reader of
the secret — “the game's mutable state is read from the system of record; the cache is trusted only for
the immutable secret on the scoring path” (submit/3's doc). A re-delivered guess re-scores to the
same number, because the engine is pure; a guess for an unknown game answers :ok — a drop, never
a retry loop.
Acceptance — “a guess submitted on the lane is scored and reaches the
leaderboard”: stories/rooms-and-games.stories.md, generated from the as-built BDD suite.
Money and the secret
the floor · privacy.stories.mdThe floor is where the two sensitive things rest, and the layers above are shaped so neither travels. The secret is a Postgres column read through the near-cache on the scoring path only; no view returns it — “the game view never carries the secret” is a generated acceptance scenario, not a promise. Money is relational: the three currencies “mutate atomically in the database through a wallet with a transaction ledger,” and the diamond prize pools settle through the second queue, not through the surface.
What crosses the layers is identities and messages about them — a GAM,
a PLR, a JOB, a GES, and a scored event carrying a name and a
percentage. That is the branded-component law applied: the boundaries are real, and the same code runs whether the
systems share a node or not.
Acceptance — stories/privacy.stories.md: “the game view never carries
the secret” · “a player's history shows only their own attempts” · “the
leaderboard exposes scores, never guess content.”
References
sources · relatedSources
- Kreps — The Logthe bus as the ordered log work is dropped onto and drained from.
- Helland — Life Beyond Distributed Transactionsentities behind boundaries and the privacy seam the views keep.
- Erlang/OTP — supervisorthe dependency-ordered tree that stands the four layers up.
Related
- C0.2 — The engine and its policiesthe previous dive: one engine, modes as policy.
- C1 — The Game as Branded Systemsthe identity law that threads the four layers, next.
- EchoMQ — the busthe queue, lane, and event protocol layer 3 runs on.
- BCS — the architecture lawsystems own state; identities and messages cross boundaries.