Skip to main content

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:

  1. Name events as past-tense domain facts: orders.paid.v1, users.email_changed.v1.
  2. Define an envelope (CloudEvents is a good default) and stick with it.
  3. Use JSON Schema or Avro for payloads; version payload schemas per event type.
  4. Write an AsyncAPI document and check it into the producer's repo.
  5. Document delivery semantics (ordering, at-least-once / exactly-once, retention) in prose alongside the spec.
  6. Treat event-type changes with the same review gate as REST breaking changes (Cluster 5).

Check Yourself

  1. Why is "at-least-once" delivery the common default, and what does that force consumers to do?
  2. 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?
  3. 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:

  1. An AsyncAPI document for at least three event types (created/updated/deleted or domain-specific actions).
  2. A prose paragraph documenting delivery guarantees: ordering, dedupe, retention, replay.
  3. One worked example of a breaking vs non-breaking change to an event payload, citing the compatibility rules.

Read This Only If Stuck