Files
hq/02-DECISIONS/0041-events-are-a-relationship.md
T
jschoubben 7b4916e9ec Modules declare their own seats; the mesh reserves mesh-*
The architecture 0117 opened needs a module to offer a service as a role on
the bus — one holder, addressed by what it does. A closed table in the
controller cannot express that: a capability a module contributes would
require changing the mesh itself.

But 0110 closed the set for a good reason — nothing could say what seats a
mesh had, and the hand count came out at eleven of thirteen. That argues for
enumerable, not hardcoded, and 0110 weighed free-form against a fixed table
without considering a third option: closed at any moment and derived from
the catalogue. A derived list cannot drift, which is how the count broke.

So: the mesh's seats stay the mesh's, reserved by the mesh- prefix so the
prefix is the rule and there is no list to maintain; ten seats are renamed
to restore 0079's convention; everything 0110 decided about what a seat IS
survives untouched.

Design 29 carries the declaration model: three namespaces, subjects derived
from local names so a manifest survives the wire changing, queues never
declared, five relationships (the job and state shapes 0041 had no room
for), and the build-publish-deploy lifecycle with hard, soft and build-time
dependencies distinguished.

0041 gets a progressive insight: "no per-consumer setup, only a
subscription" was a fact about a topic exchange, and a JetStream durable
consumer is a real object someone creates.

WBS 1.3/1.4 were wrong and say so: streams come at registration and
consumers at assignment, so only the foundation set belongs at genesis.
2026-09-26 20:34:32 +02:00

5.3 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
what runs on it accepted 2026-09-03 jochen false 0040-what-a-module-is.md

41. Events are a relationship, the lighter sibling of provisioning

Context

ADR 0040 names two relationships between modules — seats and provide/require (provisioning). A third is latent in the mesh and worth making first-class: the broker every node already runs (ADR 0002) can carry a module's activity as events, which any other module reacts to. A logger that writes an audit trail, a module that acts when another module acts, observability — all of it is one mechanism, and today it is ambient rather than declared.

Decision

A module emits events and consumes events, and both are declared — parallel to provides / requires, so the mesh knows the event graph the same way it knows the provisioning graph.

Events are provisioning's lighter sibling

provisioning events
shape 1:1, a provider creates a resource for one consumer 1:many, a module emits, any number listen
credential yes — sealed, per consumer none — it is broadcast
machinery a provisioner (the reconcile adapter) nothing but the broker's topic routing
declared as provides / requires emits / consumes

Because an event is broadcast and credential-free, there is no provisioner and no per-consumer setup — only a subscription. That is why it is the lighter relationship, and why most inter-module reaction should be an event, not a provision.

An event carries what an audit needs

Every event carries its type (a dotted topic key, so listeners match by prefix), its source module, the node it came from, and the time. A body follows. The metadata is not optional: a reaction may only need the body, but an audit trail needs to know who did what, where and when, and an event that cannot answer that is not auditable.

The audit logger is just a consumer of everything

A logger that records the whole mesh's activity is not a privileged component — it is an ordinary module that consumes # (every event) and writes them down. It holds no special access; it only listens widely. That it falls out of the model with no new machinery is the check that the model is right.

consumes is validated like requires

A consumes for an event that nothing emits is a dangling edge, and the mesh refuses it before deploy — the same rule that catches a requires for a resource nothing provides (research 011). A listener waiting for an event that can never arrive is a silent failure, and this repository's whole discipline is against silent failure.

One runtime serves all three

The per-node module runtime that serves a module's tools also wires its consumes (subscribe, dispatch to the handler) and lets its code emit. Tools are invoked (request/reply), resources are provisioned (1:1, credentialed), events are emitted and consumed (1:many, broadcast) — three relationships, one broker, one runtime, all declared on the manifest.

Consequences

  • The mesh gains a declared event graph alongside the provisioning graph — visible, validated, reasoned over.
  • Reaction becomes the default coordination: a module acts on another's event without either knowing the other, and without a credentialed link. Coupling drops.
  • An audit trail is a module, not a platform feature — and can be swapped, extended or run more than once (a file logger and a queryable one) with no change to anything that emits.
  • The runtime must dispatch a module's event handlers as well as its tools; that generalisation is small (both arrive by importing the module's entrypoint) but it is real work.

Progressive insight

Progressive insight — 2026-09-26. "No provisioner and no per-consumer setup" was a fact about the transport, and the transport changed. This record's table says an event's machinery is "nothing but the broker's topic routing", and the text that an event needs "no per-consumer setup — only a subscription". That was true of a topic exchange, where a binding cost nothing and the broker fanned out. On NATS (ADR 0106) a subscription is a durable consumer: a real object with a name, an ack policy, a delivery limit and its own ack subject, created when a module is assigned and removed when it is not. Per-consumer setup exists, and the controller does it.

The decision is untouched — events are declared on both sides, 1:many, credential-free, and still provisioning's lighter sibling; the lightness is now relative rather than absolute. ADR 0118 adds the relationship this record's two columns had no room for: work addressed to a role, where exactly one holder must act.

References

  • ADR 0002 — the broker events ride.
  • ADR 0040 — the relationships this extends.
  • ADR 0039 — emit/on are stable sdk surface; the broker binding and the runtime are not.
  • research 011 — the graph these edges join.