Appendix C - 4. Sole raw-Redis seam (the dispatch module)
The Structure of Sole raw-Redis seam (the dispatch module) — its shape at a glance:
Every caller reaches the store through the one dispatch module. That module declares the key schema and encodes atomic pop-and-move. A sole-seam lint bans the raw client anywhere else.
flowchart LR
P[Producers] --> Seam
C[Consumers] --> Seam
M[Metrics] --> Seam
Seam["Dispatch module<br/>(key schema + atomic ops)"] --> R[(Store)]
P -. banned raw client .-> R
L{{Sole-seam lint}} -. fails build .-> P
Accessible description: producers, consumers, and metrics all reach the store through the one dispatch module, which declares the key schema and encodes atomic pop-and-move. A dashed edge marks a caller using the raw client directly; the sole-seam lint fails the build on it.
Projected from the catalogue entry canonical-models-and-seams / Sole raw-Redis seam (the dispatch module).
On this page: Intent · Motivation · Applicability · Structure · Sample Code · Consequences · Example use within DocAble · Related Patterns
Intent
Intent — Confine all raw-Redis access to one module, with every queue key declared there, so the queue's atomicity and schema invariants live in exactly one lint-enforced place.
Motivation
Raw Redis calls scattered across the codebase break two invariants at once. Atomicity: a pop-and-move done as ZPOPMIN + LPUSH (two commands) silently loses a job if the worker crashes between them. Schema: key names drift from the declared set, so a metric that reads queue depth misses a structure. Both recur wherever someone reaches for the raw client.
Applicability
- A single module owning Redis and a declared key schema it is the source of truth for.
- Atomic operation primitives (Lua /
RPOPLPUSH) so pop-and-move can't be two commands. - A sole-seam lint banning the raw client elsewhere.
Structure
The Structure diagram appears at the top of this page.
Sample Code
The seam's job is to make the non-atomic pop-and-move unwritable. A two-command sequence (pop from one structure, push to another) loses work on a crash between them; the seam runs the move as one atomic server-side script, so no caller can express the racy version. Every key the seam touches comes from one declared table.
KEYS = {"ready": "q:ready", "processing": "q:processing"} # the one key schema
# atomic pop-and-move: pops the lowest-priority member and records it as processing
# in a single server-side step, so a crash can never lose the job between two commands.
_MOVE = """
local job = redis.call('ZPOPMIN', KEYS[1])
if job[1] then redis.call('HSET', KEYS[2], job[1], ARGV[1]) end
return job[1]
"""
class DispatchSeam:
"""The only module that speaks to the raw store. Callers get crash-safe verbs;
they never hold the raw client, so they cannot write a two-command pop-and-move."""
def __init__(self, client):
self._client = client # private — never exposed to callers
self._move = client.register_script(_MOVE)
def claim_next(self, worker_id: str) -> str | None:
return self._move(keys=[KEYS["ready"], KEYS["processing"]], args=[worker_id])
Consequences
- All Redis funnels through one seam — a coupling point that must cover every queue operation.
- Transitional debt. The flat re-export shim for the key schema is a name to eventually retire.
- The seam must keep pace with new queue structures, or callers are tempted back to raw access.
Example use within DocAble
- The dispatch module (sole raw-Redis seam) + the queue-key schema.
- Atomic pop-and-move (Lua
EVALfor ZSETs,RPOPLPUSHfor lists). - The sole-raw-Redis-seam lint.
Related Patterns
- See also (sibling) — service-client: the same
bounded-servicepattern for the cross-service-HTTP boundary: one lint-enforced seam owning a dangerous class of raw calls. - Counterpart — the sole-seam lint (hard) that bans raw Redis outside the dispatch module.