Files
hq/02-DECISIONS/0041-events-are-a-relationship.md
T
jschoubben ce6ae943b7 Merge main: renumber this branch's records around the trunk's
Both lines of work numbered from the same point, so four decision records and one design
document existed twice with different content. The trunk keeps its numbers and this branch
yields — the only rule that scales, because the trunk's are already cited by what merged
before them.

  0117 the bus is the only broker        -> 0125
  0118 a module declares its own seats   -> 0126
  0119 amqp is a provision, not the bus  -> 0127
  0120 the mesh bus is required          -> 0128
  0123 a seat carries its role's protocol -> 0129
  0124 the predecessor is ending          -> 0130
  design 29, what a module declares       -> design 32

Applied to the code repositories too, because a stale reference is worse when numbers
collide than when they dangle: the reader lands on a real record that decided something
else.

Two reconciliations the merge forced, both real:

**0110 was marked wholly superseded and was not.** Its successor says in as many words that
everything 0110 decided about what a seat *is* stands untouched — and two records that
landed on the trunk rest on exactly that part. So it is accepted again, extended rather than
replaced, with a note saying which of its claims moved and where.

**A seat's protocol becomes columns, not fields.** The trunk moved the seat set out of
compiled code into a table the controller owns. This branch had added what a role accepts,
emits and serves to the Go slice. The decision is unaffected and the mechanism is better for
it: giving a role a protocol is now a write rather than a rebuild, which is the trunk's own
argument applied to what this branch added.

One check still fails and it fails on main too: a record resting on ADR 0112 while that is
still 'proposed'. Left alone — it is not this merge's to answer.
2026-09-27 18:23:41 +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 0126 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.