Flow
Decisions

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.go for the publisher to embed.
  • Deploy pipeline runs busctl plan, gates destructive changes behind ManualValidation@0, then busctl apply before Helm.
  • Local development uses the Service Bus emulator (./localenv, docs/local-dev.md). ./localenv orchestrates 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.

On this page