API-First: Messaging

This section documents Spring Boot 4.1.x and Spring Framework 7.0.x on the Java 17/21+ baseline, as described by the official Spring Boot reference documentation and each sub-project’s own site (Spring Data, Spring for Apache Kafka, Micrometer, Project Reactor, springdoc-openapi, Spring gRPC, and the others listed in this section’s Bibliography) — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Spring Boot and its ecosystem continue to evolve; the examples here target the current 4.1.x / 7.0.x releases.

This section’s bibliography lists the reference material consulted while preparing these pages.

Contract-first messaging treats a message’s shape and the topic that carries it as a versioned API, published and validated before any producer or consumer code runs. This page covers defining that contract with Apache Avro schemas, enforcing compatibility with Confluent Schema Registry, and documenting topics with AsyncAPI; for the Kafka producer/consumer runtime code itself see Messaging with Kafka.

Why contract-first for messaging

Unlike a REST endpoint, a Kafka topic has no compiler-checked interface between producer and consumer — both sides agree on a byte layout by convention only. Contract-first messaging closes that gap the same way an OpenAPI document does for REST: a schema file is the single source of truth, Java classes are generated from it rather than hand-written, and a registry rejects any change that would break a deployed consumer. The three pieces fit together as follows:

  • Apache Avro defines the record shape (fields, types, defaults) in a .avsc file.

  • Confluent Schema Registry stores every version of that schema and enforces a compatibility mode so producers and consumers can evolve independently.

  • AsyncAPI describes the topic itself — its name, the operations (publish/subscribe) allowed on it, and which schema version its messages carry — as a single spec that can generate human-readable documentation.

Defining the contract with Apache Avro

An Avro schema is a JSON document describing a record: its full name, namespace, and typed fields. Save it under src/main/avro/ (the `avro-maven-plugin’s default source directory):

{
  "type": "record",
  "name": "OrderPlaced",
  "namespace": "com.example.orders.avro",
  "doc": "Emitted when a new order is accepted for processing.",
  "fields": [
    { "name": "orderId", "type": "string" },
    { "name": "customerId", "type": "string" },
    { "name": "totalAmount", "type": "double" },
    { "name": "currency", "type": "string", "default": "EUR" },
    {
      "name": "status",
      "type": { "type": "enum", "name": "OrderStatus", "symbols": ["PLACED", "CONFIRMED", "CANCELLED"] },
      "default": "PLACED"
    },
    { "name": "placedAt", "type": { "type": "long", "logicalType": "timestamp-millis" } }
  ]
}

Every non-key field that might disappear or change meaning in a later revision should carry a default — that default is what lets Schema Registry classify a later change as backward- or forward-compatible (see below). See the Apache Avro documentation for the full type system, including unions (nullable fields via ["null", "string"]), logical types, and schema references.

Generating Java classes with avro-maven-plugin

The avro-maven-plugin compiles every .avsc file into a generated, immutable Java class (a SpecificRecord) during the generate-sources phase, so the class is available to application code without being checked into version control:

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.avro</groupId>
      <artifactId>avro-maven-plugin</artifactId>
      <version>1.12.0</version>
      <executions>
        <execution>
          <id>generate-avro-sources</id>
          <phase>generate-sources</phase>
          <goals>
            <goal>schema</goal>
          </goals>
          <configuration>
            <sourceDirectory>${project.basedir}/src/main/avro</sourceDirectory>
            <outputDirectory>${project.basedir}/target/generated-sources/avro</outputDirectory>
            <stringType>String</stringType>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

The generated com.example.orders.avro.OrderPlaced class exposes a fluent newBuilder() and works directly as the value type of a KafkaTemplate<String, OrderPlaced> — combined with the Avro serializer, this is the same producer covered in Messaging with Kafka, just typed against a generated class instead of a hand-written POJO:

{
  "orderId": "ord-482910",
  "customerId": "cust-1042",
  "totalAmount": 129.90,
  "currency": "EUR",
  "status": "PLACED",
  "placedAt": 1735689600000
}

The JSON above is the logical record the generated OrderPlaced class represents; on the wire, Kafka’s Avro serializer encodes it in Avro’s compact binary format and prefixes it with the schema’s registry ID (see next section), not as JSON.

Confluent Schema Registry

Schema Registry is a separate service that stores every schema version registered under a subject (by convention, <topic>-value for the record value and <topic>-key for the key) and validates each new registration against the subject’s configured compatibility mode before accepting it. Point the application at it and let the serializer auto-register schemas during development:

spring:
  kafka:
    producer:
      properties:
        schema.registry.url: http://localhost:8081
        auto.register.schemas: true
        value.subject.name.strategy: io.confluent.kafka.serializers.subject.TopicNameStrategy
    consumer:
      properties:
        schema.registry.url: http://localhost:8081
        specific.avro.reader: true

In production, disable auto.register.schemas and register schemas explicitly as part of a deployment pipeline, so an incompatible change fails the pipeline instead of silently landing on a broker.

Compatibility modes

The registry evaluates every new schema version against the compatibility mode set for its subject:

# Example: setting a subject's compatibility mode via the Schema Registry REST API
# PUT /config/OrderPlaced-value
compatibility: BACKWARD
  • BACKWARD (the default) — a new schema can be used to read data written with the previous schema. Consumers upgrade first; adding an optional field with a default, or removing a field that had a default, is safe.

  • FORWARD — data written with the new schema can be read with the previous schema. Producers upgrade first; this is the mirror case of BACKWARD.

  • FULL — both BACKWARD and FORWARD hold simultaneously; the safest and most restrictive mode.

  • _TRANSITIVE suffixes (BACKWARD_TRANSITIVE, FORWARD_TRANSITIVE, FULL_TRANSITIVE) check the new schema against all previous versions of the subject, not only the immediately preceding one.

  • NONE disables checking entirely — only acceptable when producers and consumers always deploy together.

Choosing BACKWARD (consumers upgrade first) is the common default for event-driven systems, since it lets a new consumer version be rolled out ahead of producers that haven’t started emitting the new fields yet. See the Confluent Schema Registry documentation for the full compatibility matrix and the REST API used to query and set it.

Describing topics with AsyncAPI

Where Avro documents the payload, AsyncAPI documents the topic it travels on: its name, which operations (publish, subscribe, or both) are allowed against it, and which message schema each operation carries. An AsyncAPI document is YAML (or JSON) following its own versioned spec:

asyncapi: 3.0.0
info:
  title: Orders Service Events
  version: 1.0.0
  description: Events published when an order changes state.

servers:
  production:
    host: kafka.example.com:9092
    protocol: kafka

channels:
  orderPlaced:
    address: orders.order-placed.v1
    messages:
      OrderPlaced:
        $ref: '#/components/messages/OrderPlaced'

operations:
  publishOrderPlaced:
    action: send
    channel:
      $ref: '#/channels/orderPlaced'

components:
  messages:
    OrderPlaced:
      name: OrderPlaced
      contentType: application/avro
      payload:
        $ref: 'schemas/order-placed.avsc'
  schemas: {}

The payload.$ref can point directly at the same .avsc file used by avro-maven-plugin, so the topic contract and the payload contract are kept in a single source rather than duplicated. See the AsyncAPI documentation for the full specification, including message traits, bindings (Kafka-specific metadata such as partition key), and multi-channel documents.

Generating HTML documentation

The @asyncapi/html-template generator turns an AsyncAPI document into a static, browsable HTML site — similar to how springdoc-openapi renders Swagger UI from an OpenAPI document, but for asynchronous APIs. It runs as a Node.js CLI (@asyncapi/generator), which a Maven build can invoke via frontend-maven-plugin so documentation generation is part of the normal build rather than a separate manual step:

<plugin>
  <groupId>com.github.eirslett</groupId>
  <artifactId>frontend-maven-plugin</artifactId>
  <version>1.15.1</version>
  <executions>
    <execution>
      <id>install-node-and-npm</id>
      <goals>
        <goal>install-node-and-npm</goal>
      </goals>
      <configuration>
        <nodeVersion>v20.15.0</nodeVersion>
      </configuration>
    </execution>
    <execution>
      <id>generate-asyncapi-docs</id>
      <phase>generate-resources</phase>
      <goals>
        <goal>npx</goal>
      </goals>
      <configuration>
        <arguments>
          -y @asyncapi/generator@latest src/main/asyncapi/orders-events.yml @asyncapi/html-template@latest
          -o target/asyncapi-docs --force-write
        </arguments>
      </configuration>
    </execution>
  </executions>
</plugin>

The generated target/asyncapi-docs/index.html lists every channel, operation, and message schema in the document — useful as a published reference for teams consuming the topic who never need to open the .avsc or .yml sources directly.

Putting the pieces together

A typical contract-first workflow moves through the three tools in sequence:

flowchart LR A["Author .avsc schema\n(Apache Avro)"] --> B["avro-maven-plugin\ngenerates Java classes"] A --> C["Register schema\n(Confluent Schema Registry)"] C --> D["Compatibility check\n(BACKWARD / FORWARD / FULL)"] A --> E["Reference schema from\nAsyncAPI channel document"] E --> F["@asyncapi/html-template\ngenerates HTML docs"] B --> G["Producer / consumer code"] D --> G

The schema file is the one artifact all three tools share: it drives code generation, it is what gets versioned and compatibility-checked in the registry, and it is what the AsyncAPI document references when describing the topic to human readers.

See also

  • Messaging with Kafka — KafkaTemplate, @KafkaListener, and the runtime producer/consumer configuration that carries these schemas.

  • REST APIs — the OpenAPI/springdoc equivalent for contract-first synchronous APIs.