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.

Kafka topic to AsyncAPI 3.0 object mapping A Kafka topic with partitions maps to an AsyncAPI channel; a send operation references the channel; a message defines payload and key schemas. Kafka topic orders.created 3 partitions channel address + bindings operation send → channel $ref message payload + key

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:

  • channels describe the medium — for Kafka, one channel per topic, with the topic name in address and broker details in bindings.kafka.
  • operations describe what your application does on a channel — send (publish) or receive (consume) — and reference the channel by $ref.
  • messages describe 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.