Flow
Decisions

0006 — No runtime schema registry

Event schemas are generated artifacts checked in CI, not a running service.

Decided. Event payload schemas are generated from the entity spec, committed to the repo, and enforced in CI. There is no registry service, no KV buckets, no startup handshake.

Alternatives

  • A first-party registry service backed by KV (legacy: a NATS service with schemas and manifests buckets, a startup manifest handshake, and a compatibility endpoint).
  • An external registry — Confluent, Apicurio, Buf.

Why

Separate two things the legacy design conflated: schemas (checked artifacts) and a registry (a running service with storage and an API). Nearly all the value is in the artifacts, and the primary compatibility check costs nothing.

The events/ package is outside internal/ because it is a published contract. A consumer imports it directly:

import accountingevents "dev.azure.com/lifeinflow/Flow/flow-technology/services/accounting/events"

var ev accountingevents.AccountCreated
json.Unmarshal(env.Data, &ev)

The compiler is the compatibility check. The producer renames a field, changes a type, or removes something the consumer references — the consumer stops building. No tooling, no schema diff, no CI step needed for this. Producers and consumers live in one module, so the import is always resolvable and always current.

Worth noting: the legacy registry's compatibility check is a stub that accepts every change. Its own comment says so. So dropping the service loses nothing currently enforced; we are building the gate for the first time either way.

What CI enforces

  1. Regenerated schemas match what is committed.
  2. Diffs within an event_type are additive only — no field removed, renamed, retyped, or newly required. Breaking changes become a new event_type.
  3. No pii:"true" field appears in an event type declared contains_pii = false.
  4. Golden fixtures: marshal a fixed value, compare bytes. This is what catches a renamed JSON tag before the rolling deploy window does.

Consequences

  • Consumers must decode leniently. Never DisallowUnknownFields on event decoding — that turns every producer's additive change into a consumer outage.
  • The schema is derived from the Go struct, so it cannot drift from the code.
  • Cross-repo consumers would lose the build-time check. An argument for keeping producers and consumers in one repo.

On this page