AsyncAPI and Event-Driven Contracts
What This Concept Is
Event-driven APIs expose topics and event types rather than request/response endpoints. A producer emits events; one or more consumers subscribe. The "API" is the set of topics, the payload schemas, and the delivery guarantees that the broker provides.
AsyncAPI is to event-driven systems what OpenAPI is to REST: a machine-readable specification for message-driven contracts. It describes:
- servers (Kafka cluster, AMQP broker, MQTT broker, etc.)
- channels (topics, queues)
- operations (publish, subscribe)
- messages with payload schemas (often JSON Schema or Avro)
- security schemes and bindings per protocol
The contract you write in AsyncAPI is what consumers build against; the broker and your producer code are implementation details beneath it.
Why It Matters Here
Most non-trivial systems end up with at least one event bus. Without a contract, event-driven architectures become "Whatever we happen to emit at 3am":
- payload shapes drift because nobody reviewed them
- nobody knows what events exist except by grep
- one consumer breaks because a field silently disappeared
- the broker topology is the only documentation
Treating events as first-class contracts - versioned, reviewed, documented - is the cure. AsyncAPI is the format that lets you treat them the same way you treat HTTP endpoints.
Concrete Example
The event itself (envelope + payload)
Convention: an event has a stable envelope and a typed payload.
{
"id": "evt_01HX6QZ2...",
"type": "orders.paid.v1",
"source": "orders-service",
"time": "2026-04-10T15:20:00Z",
"specversion": "1.0",
"datacontenttype": "application/json",
"subject": "ord_1",
"data": {
"order_id": "ord_1",
"customer_id": "c_9",
"total_minor": 4599,
"currency": "USD",
"paid_at": "2026-04-10T15:19:58Z"
}
}
The envelope here is CloudEvents. type includes a version suffix (.v1) so the payload schema can evolve per-event-type.
AsyncAPI document
asyncapi: 3.0.0
info:
title: Orders events
version: 1.2.0
description: Domain events emitted by the Orders service.
servers:
prod-kafka:
host: kafka.prod.example.com:9092
protocol: kafka
description: Production Kafka cluster.
channels:
orders.events:
address: orders.events
messages:
OrderPaidV1:
$ref: "#/components/messages/OrderPaidV1"
OrderCancelledV1:
$ref: "#/components/messages/OrderCancelledV1"
operations:
publishOrderEvents:
action: send
channel:
$ref: "#/channels/orders.events"
messages:
- $ref: "#/components/messages/OrderPaidV1"
- $ref: "#/components/messages/OrderCancelledV1"
consumeOrderEvents:
action: receive
channel:
$ref: "#/channels/orders.events"
components:
messages:
OrderPaidV1:
name: OrderPaid
title: Order paid (v1)
contentType: application/cloudevents+json
headers:
type: object
properties:
type: { type: string, const: "orders.paid.v1" }
payload:
$ref: "#/components/schemas/OrderPaidPayload"
schemas:
OrderPaidPayload:
type: object
required: [order_id, customer_id, total_minor, currency, paid_at]
properties:
order_id: { type: string }
customer_id: { type: string }
total_minor: { type: integer, minimum: 0 }
currency: { type: string, pattern: "^[A-Z]{3}$" }
paid_at: { type: string, format: date-time }
This gives consumers:
- where to connect (
prod-kafka) - what channel to subscribe to
- which event types arrive on it
- the schema for each type
Delivery guarantees - document them
AsyncAPI describes shape; it does not describe semantics. Add a section:
## Delivery semantics
- At-least-once: consumers MUST dedupe by event `id`.
- Partitioned by `subject` (order id) for per-order ordering.
- Ordering: only within a partition; not globally.
- Retention: 7 days; older events are not replayable without a backfill request.
- Schema changes follow the module compatibility rules (Cluster 5).
Without this text, consumers cannot safely design their handlers.
Common Confusion / Misconception
"Events don't need schemas because they're internal." Schemas stop the silent-break failure mode. Without them, a producer renaming a field is a production incident on consumers the producer does not know about.
"Broker = contract." The broker is transport. The contract is the event type, payload schema, and delivery semantics. Swapping Kafka for Pub/Sub should not break consumers if the contract is written at the right level.
"One topic per consumer." That is the opposite of event-driven. One topic per event type (or per aggregate) published by the producer; any number of consumers subscribe. Coupling the producer to specific consumers defeats the decoupling event-driven is supposed to buy you.
"Adding a field to an event is always safe." Only if consumers treat unknown fields as optional (most JSON/Avro parsers do, but schema-validating consumers can reject them). Publish the compatibility policy (Cluster 5) and pin consumer parser configs accordingly.
"We'll just use a shared DTO library between services." That is the opposite of contract-driven design. It couples producer and consumer releases. The schema file is the contract; generated types are implementations.
How To Use It
When introducing event-driven contracts:
- Name events as past-tense domain facts:
orders.paid.v1,users.email_changed.v1. - Define an envelope (CloudEvents is a good default) and stick with it.
- Use JSON Schema or Avro for payloads; version payload schemas per event type.
- Write an AsyncAPI document and check it into the producer's repo.
- Document delivery semantics (ordering, at-least-once / exactly-once, retention) in prose alongside the spec.
- Treat event-type changes with the same review gate as REST breaking changes (Cluster 5).
Check Yourself
- Why is "at-least-once" delivery the common default, and what does that force consumers to do?
- A new consumer is added to a topic that has 7 days of retention. Why might the consumer still see inconsistent state, and how do you fix it?
- What is the difference between versioning the topic name (
orders.events.v2) vs versioning the event type name (orders.paid.v2)?
Mini Drill or Application
Design the event side of the API you built in Clusters 2-3. Produce:
- An AsyncAPI document for at least three event types (created/updated/deleted or domain-specific actions).
- A prose paragraph documenting delivery guarantees: ordering, dedupe, retention, replay.
- One worked example of a breaking vs non-breaking change to an event payload, citing the compatibility rules.
Read This Only If Stuck
- Geewax: Request deduplication (idempotency - relevant to event dedupe)
- Geewax: Response caching and request ID collisions
- Learning DDD: Event-driven architecture
- Learning DDD: Types of events and distributed ball of mud
- Fundamentals of Software Architecture: Event-driven architecture style
- AsyncAPI Specification - canonical external reference
- CloudEvents Specification - portable envelope