Modeling Kafka Topics in AsyncAPI 3.0
Kafka topics carry no schema of their own, so the contract that producers and consumers agree on lives entirely in your AsyncAPI document. This guide shows how to map a Kafka topic to an AsyncAPI 3.0 channel, attach the protocol-specific bindings.kafka block (topic, partitions, key), wire up operations, and reference reusable message schemas. It is part of the AsyncAPI event-driven patterns section and the broader OpenAPI & AsyncAPI Schema Authoring guide.
If you are still deciding whether Kafka belongs in AsyncAPI at all rather than OpenAPI, read AsyncAPI vs OpenAPI for event-driven architectures first; the short answer is that only AsyncAPI models broker topology natively.
Problem & Context
A Kafka topic is just a named, partitioned log. Nothing in the broker tells a new consumer what bytes to expect, which field is the partition key, or how many partitions exist. Teams that skip a formal contract end up reverse-engineering payloads from production messages and discovering ordering bugs the hard way.
AsyncAPI 3.0 solves this by separating three concerns that earlier versions blurred:
channelsdescribe the medium — for Kafka, one channel per topic, with the topic name inaddressand broker details inbindings.kafka.operationsdescribe what your application does on a channel —send(publish) orreceive(consume) — and reference the channel by$ref.messagesdescribe the payload and Kafka key, ideally as reusable components.
The 3.0 split matters: in AsyncAPI 2.x a channel mixed publish/subscribe verbs whose meaning depended on perspective, which made shared documents ambiguous. In 3.0 the operation lives outside the channel and uses an unambiguous action. The rest of this guide builds one topic end to end.
Step-by-Step Solution
1. Install the AsyncAPI CLI
Pin to the AsyncAPI CLI so validation matches the 3.0 spec.
npm install -g @asyncapi/cli
asyncapi --version
Expected output (version will vary; any 2.x line of the CLI supports spec 3.0.0):
@asyncapi/cli/2.16.0 linux-x64 node-v20.11.1
2. Declare the document and the channel
The channel address is the literal Kafka topic name. Reference messages by $ref so the payload definition stays in components.
asyncapi: 3.0.0
info:
title: Orders Service
version: 1.0.0
servers:
production:
host: kafka.internal:9092
protocol: kafka
channels:
ordersCreated:
address: orders.created
messages:
OrderCreated:
$ref: '#/components/messages/OrderCreated'
3. Add the Kafka channel binding
The bindings.kafka block on the channel documents broker topology — partition count, replication, and an explicit topic if the address is a logical alias. Pin bindingVersion so tooling resolves the right binding schema.
bindings:
kafka:
topic: orders.created
partitions: 3
replicas: 3
bindingVersion: '0.5.0'
Validate after each addition (see step 6). A missing bindingVersion is the most common lint warning here.
4. Define the operation
Operations are top-level in 3.0. A producer document uses action: send and points at the channel with a runtime $ref.
operations:
publishOrderCreated:
action: send
channel:
$ref: '#/channels/ordersCreated'
messages:
- $ref: '#/channels/ordersCreated/messages/OrderCreated'
Use action: receive instead in a consumer’s document. Keep producer and consumer documents separate when they own different operations.
5. Set the message and key bindings
The Kafka key decides partition assignment and per-key ordering, so document it explicitly. The bindings.kafka.key block on the message holds a JSON Schema for the key. Pull the payload schema from your shared components — the same single-source approach described in defining JSON Schema components.
components:
messages:
OrderCreated:
name: OrderCreated
title: Order created event
contentType: application/json
payload:
$ref: '#/components/schemas/OrderCreatedPayload'
bindings:
kafka:
key:
type: string
description: Order ID; drives partitioning and ordering.
bindingVersion: '0.5.0'
6. Validate the document
asyncapi validate asyncapi.yaml
Expected output on a clean document:
File asyncapi.yaml is valid! File asyncapi.yaml and referenced documents don't have governance problems.
Complete Working Example
A single self-contained asyncapi.yaml modeling the orders.created topic: channel with Kafka binding, a send operation, and a reusable message with payload and key schemas.
asyncapi: 3.0.0
info:
title: Orders Service
version: 1.0.0
description: Publishes order lifecycle events to Kafka.
servers:
production:
host: kafka.internal:9092
protocol: kafka
description: Production Kafka cluster.
channels:
ordersCreated:
address: orders.created
title: Order created channel
description: Emitted once per accepted order.
messages:
OrderCreated:
$ref: '#/components/messages/OrderCreated'
bindings:
kafka:
topic: orders.created
partitions: 3
replicas: 3
bindingVersion: '0.5.0'
operations:
publishOrderCreated:
action: send
summary: Publish an order created event.
channel:
$ref: '#/channels/ordersCreated'
messages:
- $ref: '#/channels/ordersCreated/messages/OrderCreated'
components:
messages:
OrderCreated:
name: OrderCreated
title: Order created event
contentType: application/json
payload:
$ref: '#/components/schemas/OrderCreatedPayload'
bindings:
kafka:
key:
type: string
description: Order ID; drives partitioning and ordering.
bindingVersion: '0.5.0'
schemas:
OrderCreatedPayload:
type: object
required: [orderId, customerId, amount, currency, createdAt]
properties:
orderId:
type: string
format: uuid
customerId:
type: string
format: uuid
amount:
type: integer
description: Amount in minor units (cents).
minimum: 0
currency:
type: string
pattern: '^[A-Z]{3}$'
createdAt:
type: string
format: date-time
Save it and run asyncapi validate asyncapi.yaml to confirm it passes before committing.
Gotchas & Edge Cases
Compacted topics need a non-null key. On a log-compacted topic, the key is the compaction identity. Mark the key schema as required in your team’s review checklist; a null key on a compacted topic silently breaks compaction.
partitions is documentation, not provisioning. The bindings.kafka.partitions value records the intended topology so consumers can reason about ordering. It does not create or alter the topic — Kafka topic creation stays with your infrastructure tooling. Keep the number in sync with the real topic or the contract lies.
Avoid Kafka address templating with parameters. Channel parameters and {placeholder} addresses suit subject-based brokers like MQTT or NATS. Kafka topic names are flat strings, so prefer one explicit channel per topic; dynamic address templating tends to confuse Kafka-specific code generators.
FAQ
Should the channel address be the Kafka topic name?
Yes. In AsyncAPI 3.0 the channel address holds the literal topic string the broker uses. The bindings.kafka.topic field is optional and only needed when the address is a logical name that differs from the physical topic.
How do I document the Kafka message key in AsyncAPI?
Add a bindings.kafka.key block on the message object with a JSON Schema describing the key. This tells consumers which field drives partition assignment and ordering guarantees.
Do I use send or receive for a producer in AsyncAPI 3.0?
Use send for an application that publishes to the topic and receive for one that consumes from it. Operations describe the application’s own behavior, not the broker’s, so a producer’s document uses send.
Related
- AsyncAPI event-driven patterns — parent guide for channels, operations, and bindings.
- OpenAPI & AsyncAPI Schema Authoring — the full contract-authoring guide.
- AsyncAPI vs OpenAPI for event-driven architectures — when to pick each spec.
- Correlation IDs for event tracing — trace Kafka events across services.
- Defining JSON Schema components — reuse payload schemas across messages.