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.
96 lines
5.9 KiB
Markdown
96 lines
5.9 KiB
Markdown
---
|
|
layer: to-be
|
|
status: designed
|
|
code: []
|
|
updated: 2026-09-25
|
|
decisions:
|
|
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
|
|
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
|
|
- 02-DECISIONS/0118-a-module-declares-its-own-seats.md
|
|
---
|
|
|
|
# 23 — Choosing a provider
|
|
|
|
A provision is named for what the consumer's code is coupled to
|
|
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)):
|
|
`postgres-database`, not `database`. That decides *what kind* of provider satisfies a requirement.
|
|
It does not decide *which* provider, and the mesh runs more than one of most kinds.
|
|
|
|
## Why there is a choice at all
|
|
|
|
Node-specific services delivered to the mesh is the design, not an exception. Every control-capable
|
|
node runs its own relational store, its own cache, its own object store; a single node may run
|
|
several relational stores, each raised by the module that needed a particular engine or version.
|
|
The one provision that is single today — the identity provider — already serves applications whose
|
|
home is another node. So for a given provision name there are usually several providers, one per
|
|
node, and they are **not** interchangeable: each holds different data and lives in a different
|
|
place. A consumer bound to the wrong one reads the wrong database or takes a network hop it did not
|
|
need.
|
|
|
|
Naming the kind is therefore only half of "how a consumer gets what it needs". The other half is
|
|
which provider, and it has two shapes: **consume a provider**, or **carry your own**.
|
|
|
|
## Consuming a provider
|
|
|
|
A provider is not a mesh-wide singleton. It is identified by the node it runs on together with the
|
|
module that provides it — a (node, module) pair. A consumer's requirement resolves to one such
|
|
provider, and which one is part of the **assignment**, not the manifest
|
|
([ADR 0046](../../02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md)):
|
|
the same module, assigned twice, may be served by two different providers.
|
|
|
|
**The default is co-location.** A consumer that names no provider is served by the provider of that
|
|
provision on its own node. This is the ordinary case and is meant to need nothing said — a module
|
|
that wants a database wants, almost always, the database on the machine it runs on. A mesh that
|
|
happens to run exactly one provider of a kind is simply the case where co-location and "the only
|
|
one there is" name the same thing; that is *there happens to be one*, not a mesh-wide scope written
|
|
into the provision.
|
|
|
|
**Coupling to data is named.** The exception to co-location is a consumer coupled to a *particular
|
|
provider's contents*: two modules that must share one database, or a consumer that must reach a
|
|
provider on a different node. That coupling is exactly what may not be guessed, so the assignment
|
|
names the provider. Naming it is also what makes a later move safe — the mesh knows the binding is
|
|
to that provider and not to whichever one is nearest.
|
|
|
|
**A seat names the mesh's one provider of a kind.** Where a seat delivers the provision, its holder
|
|
answers for it when several providers exist and the consumer named none. That is not picking: the
|
|
choice was made once, mesh-wide, by assigning the holder, rather than once per consumer by naming it
|
|
([ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)),
|
|
[26 — The seats](26-the-seats.md)). A named provider still wins over the seat, because a consumer
|
|
coupled to particular contents has said so.
|
|
|
|
**Ambiguity is refused, never resolved by picking.** If several providers of a kind exist, none is
|
|
named, none is co-located, and no seat delivers it, the requirement is unsatisfiable and is refused
|
|
with the candidates shown — the same stance
|
|
[ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) took
|
|
against a confidently-wrong match, applied to the instance rather than the dialect. A wrong answer
|
|
delivered quietly costs more than a refusal.
|
|
|
|
## Carrying your own
|
|
|
|
A module need not consume a provider at all. It may carry its **own** instance of an engine inside
|
|
its own composition — reachable only on the module's own network, publishing no host port, and
|
|
**not** declared as a provision. Nothing else in the mesh can see it or bind to it, and it cannot
|
|
collide with anything on a well-known port. To resolution it does not exist; it is an internal part
|
|
of the module, like any other container the module runs.
|
|
|
|
This is legitimate but it is the exception, and the design says when: **only when a genuine engine
|
|
fork or a pinned server version makes the shared provider unusable.** A module written against a
|
|
customised engine, or one that needs an extension the node's provider does not carry, has no choice
|
|
but to carry its own. A module that merely pins an old image of an ordinary engine does not — the
|
|
version on a compose file is the *server's*, and the application talks to a newer shared server
|
|
perfectly well once its data is migrated in. The rule is *share by default; embed only when a fork
|
|
or a version forces it*. Most of the per-module stores that exist in the mesh being migrated onto
|
|
are the first kind wearing the second's clothes, and consolidate onto the node's provider.
|
|
|
|
The distinction is worth stating because the two cases look identical from outside — a module with
|
|
a database either way — and the mesh must be able to tell them apart to reason about either. A
|
|
consumed provider is a binding the mesh records, rotates and can move. An embedded instance is a
|
|
private detail the mesh does not manage and must not mistake for a provider.
|
|
|
|
## What is not settled here
|
|
|
|
A provider that moves between nodes must keep its identity, so that a consumer's recorded choice
|
|
does not silently rebind to a different provider that inherited its place. That is a property the
|
|
provider lifecycle must supply, and this document names it as a requirement rather than describing
|
|
its mechanism.
|