Skip to content

Schema registry

Related: Event catalogue · Interface contracts · MOD-043 · ADR-029


Approach

Two registries, one per interface type:

Interface type Registry Format Enforcement point
Async domain events (EventBridge) Amazon EventBridge Schema Registry JSON Schema (draft-04) MOD-043 validates at publish; consumer code uses generated bindings
Sync Lambda-to-Lambda contracts OpenAPI 3.0 spec in each module's repo under docs/api/ JSON Schema (via OpenAPI components) CI validates request/response against spec using pytest + jsonschema

Avro was not chosen because it requires a separate schema registry server (Confluent or AWS Glue Schema Registry with Avro support). EventBridge is JSON-native; JSON Schema is sufficient for the event volume and complexity of this platform. Avro would add operational overhead with no benefit at this scale.

Confluent Schema Registry was not chosen because there is no Kafka in the stack.

EventBridge schema registry structure

Amazon EventBridge Schema Registry is used as follows:

  • Registry name: bank-events (one registry for the whole platform; buses are the domain boundary, not separate registries)
  • Schema naming convention: bank.{domain}.{noun}_{past_tense_verb} — matching the event name exactly (e.g. bank.core.posting_completed, bank.kyc.identity_verified)
  • Versioning: EventBridge Schema Registry auto-versions on schema change. Schemas follow semantic versioning in the schema_version field within the event payload (e.g. "1.0.0", "1.1.0", "2.0.0"). The registry version and the payload schema_version are kept in sync.

The EventBridge Schema Registry is the source of truth for event schemas. The event-catalogue.md page in this wiki is the human-readable reference — it must be kept in sync with the registry on every schema change, as part of the module's build acceptance criteria.

Schema versioning policy

Three change tiers — determines how a schema change is deployed:

Tier What changed schema_version bump Registry action Consumer action required
Additive New optional field added minor (1.0.0 → 1.1.0) New registry version; old consumers continue working None — optional field; consumers ignore unknown fields
Breaking Required field added, field removed, field type changed, field renamed major (1.0.0 → 2.0.0) New registry version; old consumers receive new events Consumer must be updated before publisher deploys the new version (consumer-first deployment)
Deprecation Field marked deprecated in schema description patch (1.0.0 → 1.0.1) New registry version with "deprecated": true on the field Consumers have 1 release cycle to migrate away from the deprecated field

Rules:

  • A schema change classified as Breaking requires the consuming modules to be updated and deployed before the publishing module. The orchestrator is responsible for enforcing deployment order.
  • The schema_version field is mandatory in every event payload and must match the registered schema version. MOD-043 rejects events where schema_version is absent.
  • Schema changes must be accompanied by an update to event-catalogue.md in the same PR.

JSON Schema structure

The following is a complete example JSON Schema for bank.core.posting_completed:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "bank.core.posting_completed",
  "description": "Emitted by MOD-001 on every successful double-entry posting committed to the ledger.",
  "type": "object",
  "required": ["event_id", "event_time", "schema_version", "trace_id", "posting_id",
               "account_id", "party_id", "amount", "currency", "direction",
               "posting_type", "ledger_balance_after", "available_balance_after",
               "jurisdiction", "idempotency_key"],
  "properties": {
    "event_id":                 { "type": "string", "format": "uuid" },
    "event_time":               { "type": "string", "format": "date-time" },
    "schema_version":           { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
    "trace_id":                 { "type": "string", "format": "uuid" },
    "posting_id":               { "type": "string", "format": "uuid" },
    "account_id":               { "type": "string", "format": "uuid" },
    "party_id":                 { "type": "string", "format": "uuid" },
    "amount":                   { "type": "string", "pattern": "^\\d+\\.\\d{2}$" },
    "currency":                 { "type": "string", "enum": ["NZD", "AUD"] },
    "direction":                { "type": "string", "enum": ["DEBIT", "CREDIT"] },
    "posting_type":             { "type": "string", "enum": ["PAYMENT", "ACCRUAL", "FX_CONVERSION", "ADJUSTMENT", "REVERSAL"] },
    "ledger_balance_after":     { "type": "string", "pattern": "^-?\\d+\\.\\d{2}$" },
    "available_balance_after":  { "type": "string", "pattern": "^-?\\d+\\.\\d{2}$" },
    "jurisdiction":             { "type": "string", "enum": ["NZ", "AU"] },
    "idempotency_key":          { "type": "string" },
    "counterparty_account_id":  { "type": "string", "format": "uuid" }
  },
  "additionalProperties": false
}

amount and balance fields are strings with fixed-point decimal format — never numbers. additionalProperties: false is set on all schemas to prevent undocumented fields silently entering the pipeline.

Code generation

The EventBridge Schema Registry supports code generation for Python (and other runtimes) via the aws-lambda-python-runtime-interface-client and the amazon-eventbridge-schema-registry-poc tooling. The bank's approach is simpler:

  • Schemas are downloaded from the registry as part of CI (aws schemas describe-schema)
  • A lightweight validate_event(schema_name, event) utility (provided by MOD-043) validates event payloads using jsonschema.validate() at the publish and consume points
  • No generated dataclasses — consumers access event fields via dict access with explicit key documentation

The MOD-043 utility interface:

# Provided by MOD-043 — import in all event publishers and consumers
from bank_platform.schema_registry import validate_event, SchemaValidationError

# At publish time (in the emitting Lambda):
def publish_posting_completed(payload: dict) -> None:
    validate_event("bank.core.posting_completed", payload)   # raises SchemaValidationError if invalid
    eventbridge.put_events(Entries=[{
        "Source": "bank.core",
        "DetailType": "posting_completed",
        "Detail": json.dumps(payload),
        "EventBusName": "bank-core"
    }])

# At consume time (in a subscribing Lambda):
def handler(event, context):
    detail = event["detail"]
    validate_event("bank.core.posting_completed", detail)    # raises → routes to DLQ on schema mismatch
    ...

OpenAPI specs for sync contracts

Every Lambda that exposes a synchronous interface (API Gateway endpoint or direct Lambda invocation via MOD-075) maintains an OpenAPI 3.0 spec at docs/api/{module_id}.yaml in its repository.

Required structure per spec:

  • info.title: module ID + interface name (e.g. MOD-001 Posting API)
  • info.version: semver, matching deployment version
  • paths: one entry per endpoint
  • components.schemas: all request and response body schemas, referenceable
  • components.schemas.ErrorEnvelope: the standard error envelope (identical in all specs)

CI validation: a pytest fixture loads the spec and validates every test request/response fixture against the relevant schema component using jsonschema. A PR that adds a new endpoint without a corresponding spec entry fails CI.

Agent rules for schema changes

When an agent adds a new event or modifies an existing event as part of a module build:

  1. Update source/pages/design/system/event-catalogue.md with the new or changed event definition.
  2. Update or create the JSON Schema file in the module's repo under schemas/{event_name}.json.
  3. Upload the schema to the EventBridge Schema Registry as part of the IaC deployment step.
  4. Bump schema_version in the event payload and in the JSON Schema title/description.
  5. If the change is Breaking: update the wiki delivery sequence handoff to note that consuming modules must be deployed first.
  6. A module PR is not accepted with a schema change that is not reflected in event-catalogue.md.