0016 — Azure Service Bus fanout topology
Per-domain fanout topics declared in .azure/bus and reconciled at deploy time.
Decided. In Azure, each domain outbox owns a fanout topic declared under
services/<name>/.azure/bus/topology.yaml. Every event that domain emits is
published to its core topic; PII event types go to a separate topic when that
domain declares piiSubscribers. Each domain lists one or more subscribers;
adding a subscriber adds a queue. Subscribers are queues reached through topic
subscriptions with forwardTo.
Local development uses the Azure Service Bus emulator in Docker Compose. The
publisher uses the same topic names as Azure so the topology is identical across
environments. cmd/gensbemulator/ writes
.localenv/generated/service-bus/Config.json from the same topology YAML at
local setup time (gitignored).
The publisher still drains every domain outbox in the service (0015); the broker topology matches that split — one fanout per domain, not one fanout for the whole service.
Alternatives
- One fanout per service. Rejected: domains in the same service would share a topic, so every subscriber would receive every domain's events and filter in application code. That couples unrelated consumers and hides ownership.
- Routing keys and binding patterns. Rejected for Azure: Service Bus fanout topics deliver every message to every subscriber on that topic; selectivity between domains is a separate topic, not a binding pattern.
- Terraform module blocks per queue. Rejected: topology changes with every service deploy and belongs beside the service, not in shared infrastructure.
Why
The legacy RabbitMQ core-events / pii-events exchanges were shared across
services. Azure deployment needs isolation at the domain outbox, explicit
subscriber queues, and a reconcile step in the deploy pipeline. A YAML
declaration checked into the service repo is the source of truth; busctl plans
and applies it through ARM.
PII separation follows 0005: a second topic when
the domain declares piiSubscribers, plus per-queue authorization rules so
consumers not cleared for PII cannot listen on PII queues.
Consequences
- Naming: topic
<service>-<domain>-fanout(core) or<service>-<domain>-pii-fanout(PII); queue<service>-<domain>-queue-<subscriber>or<service>-<domain>-pii-queue-<subscriber>. Names tie to the domain outbox that publishes, not to the consumer process. - A domain may list any number of subscribers. Appending a name creates another queue on that domain's fanout.
- Fanout and queue entity settings live per domain in
bus.<env>.yaml, not as one service-wide defaults block. Dev and prod can differ per domain. - common/bus/ derives names and validates topology;
cmd/gentopology/ renders
messageBus/topology_gen.gofor the publisher to embed. - Deploy pipeline runs
busctl plan, gates destructive changes behindManualValidation@0, thenbusctl applybefore Helm. - Local development uses the Service Bus emulator (
./localenv, docs/local-dev.md)../localenvorchestrates process-compose; emulator topology is generated from the same YAML as Azure.
Deployment checks
Every publishing domain must appear in topology.yaml, even with no subscribers,
and have a matching entry in both bus.dev.yaml and bus.prod.yaml.
busctl plan validates the selected environment; a missing entry produces
bus env missing domain "<domain>".
The domain slug need not match the service name. Accounting uses service
accounting and domain account, so its core topic is
accounting-account-fanout. Renaming the domain changes broker entity names;
keep the publisher, generated topology, and environment files aligned rather
than renaming one in isolation.
The first-deploy checklist and configuration example are in docs/devops/deploying-a-service.md.
See also the template service operations page for the concrete
template topology.