AsyncAPI, Event Contract Testing, and Governance
Learn Java Microservices Communication - Part 078
AsyncAPI, event contract testing, and governance for Java microservices: event catalogs, producer/consumer contracts, schema compatibility, fixtures, Spring Kafka/Testcontainers integration tests, CI gates, review workflows, and platform policy.
Part 078 — AsyncAPI, Event Contract Testing, and Governance
Synchronous APIs usually have explicit contracts.
HTTP APIs have OpenAPI.
gRPC APIs have .proto.
Event-driven APIs often have something weaker:
topic name + tribal knowledge + sample JSON in a wiki
That is not enough.
A topic is an API.
An event type is an API operation.
A message schema is an API payload.
A producer is an API provider.
A consumer is an API client.
If event contracts are not documented, tested, and governed, async systems drift until an incident reveals hidden coupling.
1. Async API Mental Model
An asynchronous API contract should describe:
- channels/topics,
- messages,
- producers,
- consumers,
- payload schemas,
- headers,
- keys,
- protocol bindings,
- security,
- versioning,
- compatibility,
- examples,
- ownership,
- operational expectations.
AsyncAPI is a specification for describing message-driven APIs in machine-readable form.
For Java/Kafka teams, AsyncAPI can become the event equivalent of OpenAPI.
2. AsyncAPI vs Schema Registry
Schema registry stores payload schemas and compatibility metadata.
AsyncAPI describes the API/channel contract.
| Concern | Schema Registry | AsyncAPI |
|---|---|---|
| payload schema | strong | references/includes |
| compatibility checks | strong | can reference |
| channel/topic docs | weak | strong |
| producer/consumer roles | weak | strong |
| examples | limited | strong |
| protocol bindings | limited | strong |
| security docs | limited | strong |
| human API catalog | weak | strong |
Use both.
Schema registry does not document business semantics by itself.
3. Event Contract Contents
A production event contract includes:
eventType: com.example.case.CaseEscalated.v1
topic: case-events
owner: case-platform
producer: case-service
key:
field: caseId
purpose: per-case ordering
payloadSchema: case-escalated-v1
headers:
required:
- event_id
- event_type
- correlation_id
- causation_id
compatibility: full-transitive
retention: 7d
classification: internal-confidential
replaySafe: true
consumers:
- notification-service
- search-indexer
- audit-service
Contract is operational design.
Not only JSON shape.
4. AsyncAPI Skeleton
asyncapi: 3.0.0
info:
title: Case Events API
version: 1.0.0
channels:
caseEvents:
address: case-events
messages:
CaseEscalated:
$ref: '#/components/messages/CaseEscalated'
operations:
publishCaseEscalated:
action: send
channel:
$ref: '#/channels/caseEvents'
messages:
- $ref: '#/channels/caseEvents/messages/CaseEscalated'
components:
messages:
CaseEscalated:
name: CaseEscalated
contentType: application/json
payload:
$ref: '#/components/schemas/CaseEscalatedPayload'
The important model:
channel + message + schema + operation + metadata
5. Event Catalog
An event catalog is a searchable inventory of events/topics.
For each event:
- name,
- description,
- owner,
- producer,
- topic,
- schema,
- examples,
- key policy,
- version,
- compatibility,
- consumers,
- retention,
- classification,
- SLOs,
- replay policy,
- DLQ policy,
- contact/runbook.
Event catalog improves discovery, review, onboarding, and incident response.
6. Producer Contract Tests
Producer contract test verifies:
given domain change
producer emits expected event contract
Test:
- topic,
- key,
- event type,
- event ID,
- headers,
- payload schema,
- absence of forbidden data,
- aggregate version.
Example:
@Test
void escalationEmitsCaseEscalatedEventContract() {
useCase.execute(command);
OutboxMessage event = outboxRepository.singlePending();
assertThat(event.topic()).isEqualTo("case-events");
assertThat(event.messageKey()).isEqualTo("CASE-100");
assertThat(event.eventType()).isEqualTo("CaseEscalated.v1");
schemaValidator.validate(event.payload(), "case-escalated-v1");
}
This catches contract drift at source.
7. Consumer Contract Tests
Consumer contract test verifies:
given valid fixture
consumer handles it correctly
Example:
@Test
void consumesCaseEscalatedV1Fixture() {
EventEnvelope envelope = fixture("case-escalated/v1/valid.json");
consumer.handle(envelope);
assertThat(repository.get("CASE-100").status())
.isEqualTo("ESCALATED");
}
Also test:
- unknown fields,
- missing optional field,
- unknown enum,
- old schema version,
- duplicate,
- out-of-order version,
- invalid schema.
Consumer compatibility depends on real fixtures.
8. Fixture Strategy
Store fixtures:
src/test/resources/contracts/events/
case-escalated/v1/
valid-minimal.json
valid-full.json
unknown-extra-field.json
unknown-enum.json
missing-required-case-id.json
Fixtures should be stable, reviewed, versioned, and used in docs and CI.
Fixtures are executable examples.
9. Schema Compatibility Gate
CI should check:
- new schema vs previous schema,
- transitive compatibility,
- forbidden field names,
- required field changes,
- enum changes,
- removed fields/reserved numbers,
- subject naming policy.
Schema compatibility is necessary but not sufficient.
Semantic compatibility still needs review.
10. Semantic Compatibility
Schema may pass while meaning breaks.
Example:
field remains string
but meaning changes from case status to escalation status
Consumers break.
Review:
- field meaning,
- key changes,
- event type changes,
- ordering,
- timestamp semantics,
- replay behavior,
- retention,
- privacy classification,
- new PII fields.
Human API review complements automated checks.
11. Consumer-Driven Contracts
Consumers can publish requirements.
consumer: notification-service
eventType: CaseEscalated.v1
requires:
fields:
- caseId
- escalationId
- targetQueue
headers:
- correlation_id
unknownFields: ignore
replaySafe: false
Producer checks impact before changes.
This reveals hidden dependencies.
12. Spring Kafka and Testcontainers
Use Spring Kafka tests for:
- listener container behavior,
- serialization/deserialization,
- error handler,
- DLT,
- retry topics,
- headers.
Use Testcontainers for:
- real broker integration,
- schema registry,
- producer/consumer round trip,
- Kafka Streams,
- outbox relay.
Mocks are not enough for protocol and container behavior.
13. CI Gate Template
asyncApiCi:
required:
- asyncapi-lint
- schema-compatibility
- producer-contract-tests
- consumer-fixture-tests
- header-key-tests
- forbidden-field-scan
- historical-replay-fixtures
- documentation-generation
breakingChange:
requires:
- architecture-review
- consumer-impact-list
- migration-plan
- sunset-date
- dual-publish-plan
Automate what machines can check.
Review semantics humans must judge.
14. Breaking Change Process
Breaking event change requires migration:
- create new event major version,
- publish both versions or new topic,
- update AsyncAPI docs,
- notify consumers,
- provide fixtures,
- migrate consumers,
- monitor old version usage,
- stop old version after retention/replay window,
- archive old docs/fixtures.
Breaking changes are programs, not quick PRs.
15. Event Review Checklist
Ask:
- Is event a fact or command?
- Is owner clear?
- Is topic correct?
- Is key correct?
- Is ordering scope documented?
- Is payload minimal?
- Is PII included?
- Is schema compatible?
- Are examples present?
- Are old consumers safe?
- Are replay semantics defined?
- Are producer/consumer tests updated?
- Is AsyncAPI updated?
Event review is API review.
16. The Real Lesson
Async APIs need contracts as much as synchronous APIs.
A topic is durable, replayable, shared, and consumed by many services.
Production-grade async governance requires:
AsyncAPI/event catalog
+ schema compatibility
+ fixtures
+ producer contract tests
+ consumer contract tests
+ semantic review
+ privacy review
+ CI gates
+ drift detection
That is how event-driven architecture scales across teams without becoming event-driven chaos.
References
- AsyncAPI Specification: https://www.asyncapi.com/docs/reference/specification/latest
- AsyncAPI Initiative: https://www.asyncapi.com/
- Spring Kafka Testing Reference: https://docs.spring.io/spring-kafka/reference/testing.html
- Testcontainers Kafka Guide: https://testcontainers.com/guides/testing-spring-boot-kafka-listener-using-testcontainers/
- Confluent Schema Registry — Schema Evolution and Compatibility: https://docs.confluent.io/platform/current/schema-registry/fundamentals/schema-evolution.html
- Apache Kafka Documentation: https://kafka.apache.org/documentation/
You just completed lesson 78 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.