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_versionfield within the event payload (e.g."1.0.0","1.1.0","2.0.0"). The registry version and the payloadschema_versionare 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_versionfield is mandatory in every event payload and must match the registered schema version. MOD-043 rejects events whereschema_versionis 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 usingjsonschema.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 versionpaths: one entry per endpointcomponents.schemas: all request and response body schemas, referenceablecomponents.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:
- Update
source/pages/design/system/event-catalogue.mdwith the new or changed event definition. - Update or create the JSON Schema file in the module's repo under
schemas/{event_name}.json. - Upload the schema to the EventBridge Schema Registry as part of the IaC deployment step.
- Bump
schema_versionin the event payload and in the JSON Schema title/description. - If the change is Breaking: update the wiki delivery sequence handoff to note that consuming modules must be deployed first.
- A module PR is not accepted with a schema change that is not reflected in event-catalogue.md.