jonnify·redis /redis-patterns/streams-events/custom-events/domain-events-on-the-stream

R5.04.1 · dive 1

Domain events on the stream.

A stream is identified by its name, so each kind of domain event can have its own — one append-only log per channel, read by exactly the readers that care about it.

Append a room-opened to one stream, a guess-scored to another, a game-settled to a third, and three independent logs follow. A reader of one feed reads one stream; it does not page through a combined log to find its events. Select an event type; the readout names what its stream holds and who folds it.

A named stream is a domain-event channel. Select an event type; the readout names what its stream holds and who reads it.
three named streams · one per type room-opened room ROM… room ROM… room ROM… guess-scored total 480 total 320 total 600 game-settled winner PLR… pot 60
readout · Select an event type. Each domain-event type has its own named stream — one append-only log per channel.
§1

The name is the channel

domain-events-on-the-stream.md · one log per type

XADD key field value … appends to the stream named key, creating it on first append. Choose the key by the event type and the key is the channel: every room-opened event lands on the room-opened stream, every guess-scored on the guess-scored stream. A reader of one feed reads one stream; it does not filter a combined log to find its events.

XADD room-opened   * room ROM0Nk2…  set animals  fee 1     # -> "…000-0"
XADD guess-scored  * game GAM0Nk3…  player PLR0…  total 480 # -> "…000-0" (a separate log)
XADD game-settled  * game GAM0Nk3…  winner PLR0…  pot 60    # -> "…000-0"

A new event type costs a new name and nothing else — no shared table to migrate, no enum to extend. The cost of one stream per type is a key per type; the buy is a focused log per feed, so a reader's range read returns only its own events in order.

The key is the channel: append a domain event to the stream named for its type, and one focused, append-only log per channel follows.

§2

One brand per stream keeps order sound

domain-events-on-the-stream.md · the kind door

Each entry carries a time-ordered id, and a later append always carries a larger id than an earlier one — within one stream. The order holds because the entries are minted by a single writer in increasing order; the id sort, the entry order, and the write order are the same sequence. Across two streams the ids are independent — room-opened and guess-scored each start their own sequence — so a reader folds one stream at a time, in that stream's order.

One brand per stream — the kind door admits one namespace. Select a record id; the readout reports whether the stream admits it.
the stream admits one brand: EVT EVTEVT0…xY9Wvvcdadmitted ORDORD0…xY9Wvvcdwrong kind —not-brandedmalformed
readout · Select a record id. The stream admits one brand — EVT — so byte order equals mint order; a foreign or malformed id is refused at the door, before any wire.

A stream admits one brand, so its byte-ordered ids stay in mint order; the kind door refuses a foreign or malformed id before any wire, and the writer mints its own, so a wrong kind cannot slip in.

§3

The events are facts, like any event-sourced log

domain-events-on-the-stream.md · immutable, append-only

A domain event is a fact that happened: a room was opened, a guess scored 480, a game settled to a winner. It is appended once, never edited, never deleted as a correction — a correction is a new event. The streams grow forward only, and any view over them is a fold of the facts. Custom events change what the facts are, not the rule that they are immutable and append-only.

A domain event is a fact appended once and never edited; custom events change which facts are recorded, not that they are immutable and grow forward only.

§4

In EchoMQ — append/4 and the caller-chosen name

echo_mq/stream.ex · append/4 · stream/id.ex · evt?/1

EchoMQ.Stream.append(conn, queue, name, fields) is the writer, and the name is the domain-event channel. It mints an EVT-branded record id host-side, derives the explicit XADD id from it, and issues XADD emq:{q}:stream:<name> <xadd_id> id <branded> <fields…>, returning {:ok, branded} — the branded id as the receipt. The key is emq:{q}:stream:<name> via EchoMQ.Keyspace.queue_key/2, so each named stream of one queue shares that queue's {q} hash-tag slot.

# EchoMQ.Stream.append/4 — name as the domain-event channel (real)
EchoMQ.Stream.append(conn, "cm", "room-opened",  [{"room", room}, {"set", "animals"}])
EchoMQ.Stream.append(conn, "cm", "guess-scored", [{"game", game}, {"player", player}, {"total", "480"}])
#  mints an EVT id; XADD emq:{cm}:stream:room-opened <xadd_id> id <branded> room … set animals
#  -> {:ok, "EVT0Nk2…"}    (the branded id is the receipt)

One brand per stream is enforced at the door. EchoMQ.Stream.Id.evt?/1 is the kind predicate, and EchoMQ.Stream.append_id/5 raises before any wire if a caller supplies a record id of the wrong namespace. That single-brand rule is what keeps the order theorem sound: base-62 byte order equals integer order only within one namespace, so a stream that admits one brand (EVT) keeps its byte-ordered ids in mint order. append/4 mints its own EVT id, so a wrong kind there cannot occur.

The worked consumer is codemojex (echo/apps/codemojex), a Telegram emoji-guessing game on this stack. Its lifecycle produces the domain events: Codemojex.Rooms.create_room/3 opens a room (a ROM) and the first joiner starts a game (a GAM); Codemojex.Guesses.submit/3 enqueues a guess as a JOB on the player's PLR lane, which the consumer scores with Codemojex.Scoring.score/2; the game settles to a winner. Each of those — room opened, guess scored, game settled — is one domain event appended to its own named stream, so the activity feed and the results view each fold their own channel.

Notes on ValkeyA stream is created automatically when the first entry is added to it, so a new domain-event channel is one XADD to a new key — no declaration step — valkey.io/commands/xadd.

the pattern

A stream is named, so each domain-event type gets its own; one append-only log per channel, read by the readers that care about it.

its EchoMQ application

EchoMQ.Stream.append/4 takes a caller-chosen name → XADD emq:{q}:stream:<name>; EchoMQ.Stream.Id admits one brand (EVT) per stream, keeping the id sort equal to mint order. The codemojex room / guess / settle events are one channel each.

Redis names a stream by its key; EchoMQ makes that name the domain-event channel and admits one brand per stream, so each channel is a focused, mint-ordered log.

/echomq/bus

A door, not a depth — the EchoMQ Bus pillar

This dive cites the named append as proof the domain-event channel ships. The consumer-group reader and the polyglot seam that consume those channels are the subject of the dedicated EchoMQ course. Open onto the Bus pillar — the broadcast and the retained, replayable log.

/bcs/bus

The Stream Tier in the manuscript

The figures here are drawn from the Branded Component System bus chapter. As B3, the bus puts it: the stream keys are born braced and branded like every other key on the bus, and one brand per stream is what makes the entry order, the id sort, and the mint order one sequence.

§5

References

sources · related

Sources

Related in this course