API-First with AsyncAPI

Ivan Garcia Sainz-Aja

·7 min read

API-First with AsyncAPI

If you are familiar with OpenAPI and OpenAPI Generator API-First workflow:

  • First, write the OpenAPI definition, collaborating between API providers and API consumers.
  • Then, use OpenAPI Generator, either the maven plugin or a CLI, to generate some DTOs and interfaces from your OpenAPI definition.
  • Implementing the generated interfaces, you can create a service for the API.
  • As a client, you can use generated interfaces to consume the API with some HTTP client generated behind the scenes.

When doing API-First with AsyncAPI, the process is similar. After you generate some interfaces and DTOs from your API definition, you use the generated interfaces to produce messages, send them to the broker, and implement them to consume messages from the broker.

There is still a fundamental difference between OpenAPI and AsyncAPI: OpenAPI is used to document Request-Response / Client-Server APIs, while AsyncAPI is used to document Event-Driven APIs which, except for WebSockets, are Broker-based.

And broker-based APIs, unlike Client-Server, are inherently symmetric.

Broker-based APIs are Symmetric

Client-server vs broker-based EDAs

Because APIs mediated by a broker are inherent symmetric, it's difficult to establish the roles of the client/server: what represents a publish operation from one side will be a subscribe operation seen from the other side. Also, a given service can act as a publisher and subscriber on the same API.

For these reasons, to avoid defining the same API operations multiple times from each perspective, we propose to define the API only once from the perspective of the provider of the functionality, which may be a producer, a consumer, or both.

Some definitions:

  • SERVICE: An independent piece of software, typically a microservice, that provides a set of capabilities to other services.
  • PROVIDER: The service that implements the functionality of the API. It may be accepting asynchronous command requests or publishing business domain events.
  • CLIENT/s: The service/s that uses the API's functionality. It may be requesting asynchronous commands or subscribing to business domain events.
  • PRODUCER: A service that writes a given message.
  • CONSUMER: A service that reads a given message.

Define your AsyncAPI from the perspective of the PROVIDER of the functionality, which may be a producer, a consumer, or both. Share this definition with your CLIENTS.

Use the table to understand which section of AsyncAPI (publish or subscribe) to use for each topic and which role (provider or client) to use on the plugin configuration.

EventsCommands
ProviderProduces (publish)Consumes (subscribe)
ClientConsumes (subscribe)Produces (publish)
OperationId Suggested Prefixon<Event Name>do<Command Name>

If you still find it confusing which one is a provider and a client, just use this rule: it can be only one provider of a given message, while clients of a given message there can be many:

  • If the provider is the producer, use publish section
  • If it is the consumer, use subscribe section.

Events, Commands, and Messages

There are two types of messages in a messaging system: events and commands. An event message describes a change that has already happened, while a command message describes an operation that needs to be carried out. In other words, events are used to notify subscribers about something that has already occurred, while commands are used to initiate an action or process.

  • Event: A message describing a change that has already happened.
  • Command: A message describing an operation that has to be carried out.

Also, while there can be only one provider that produces a given event, commands can be issued for one or many client producers.

Understanding AsyncAPI Definition

While OpenAPI and AsyncAPI come to document completely different architectural styles, they are similar in many aspects; in fact, AsyncAPI YAML format was initially based on OpenAPI format and structure.

If you are familiar with OpenAPI, you may find useful the following image borrowed from AsyncAPI documentation (click image to follow):

OpenAPI and AsyncAPI

Info

Document your API: name, purpose, contact details, and license...

Servers

Document where your API will be deployed and required security...

You can also document some server protocol-specific configurations using free-form bindings property

Channels: Publish / Subscribe

Each channel represents one single broker topic, channel, or queue... where you are about to publish or subscribe.

Use the table above to understand which section, publish or subscribe, you may want to use.

In a nutshell:

Providers publish events and subscribe to commands/queries/requests.

If you still find it confusing which is a provider and a client, use this rule: In a given messaging scenario, there can be only one provider of a message, while there can be multiple clients. If the provider is producing messages, use the publish section. If the provider is consuming messages, use the subscribe section.

Messages

Use Messages to describe Headers, Payload Schema, and Content-Type. You can also include examples, descriptions, and protocol-specific binding documentation...

1components:
2  messages:
3    turnOnOff:
4      name: turnOnOff
5      title: Turn on/off
6      summary: Command a particular streetlight to turn the lights on or off.
7      headers:
8        type: object
9        properties:
10          my-app-header:
11            type: string
12      payload:
13        $ref: "#/components/schemas/turnOnOffPayload"
14

Message Payloads / Schemas

You can define message payloads as:

  • Inline components/schemas in the same familiar way you do in OpenAPI
  • External files: both json-schema and avro schemas (.avsc) are supported
1components:
2  messages:
3    MessageWithAsyncAPISchema:
4      payload:
5        $ref: "#/components/schemas/turnOnOffPayload" ## asyncapi/inline schema
6    MessageWithExternalJsonSchema:
7      schemaFormat: 'application/schema+json;version=draft-07'
8      payload:
9        $ref: "some/external/file.schema" ## a json-schema file
10    MessageWithAvroSchema:
11      schemaFormat: application/vnd.apache.avro+json;version=1.9.0
12      payload:
13        $ref: "v1/imports/file.avsc" ## and avro schema file

Reusing Configurations: Operation Traits, Message Traits...

Operation Traits, Message Traits are an excellent way to reuse chunks of configuration between different operations or messages.

For instance, if various messages share some common headers, you can configure them as Message Traits:

1components:
2  messages:
3    CustomerEventMessage:
4      name: CustomerEventMessage
5      title: Async Event for a Customer
6      summary: Async Event for a Customer
7      schemaFormat: application/vnd.aai.asyncapi;version=2.4.0
8      traits:
9      - $ref: '#/components/messageTraits/CommonHeaders' # 'CommonHeaders' contents will replace 'traits' property
10      payload:
11      $ref: '#/components/schemas/CustomerEventPayload'
12
13  messageTraits:
14    CommonHeaders:
15      headers:
16      type: object
17      properties:
18      my-app-header:
19        type: integer
20        minimum: 0
21        maximum: 100

And the same concept applies to Operation Traits.

Different Styles of Event Messages

Notification Messages

An Event Notification contains minimal information about the event and enough information for interested consumers to locate additional details. The specifics of what information is included in an event notification can vary depending on the system or use case.

1{
2  "headers": {
3    "event-type": "customer-created",
4    "event-id": "",
5    "aggregate-id": "1",
6    "aggregate-type": "customer"
7  },
8  "payload": {
9    "id": 1,
10    "eventType": "created",
11    "link": "/customers/1"
12  }
13}

State Transfer Messages

On the other hand, a State Transfer message contains the entire state of the aggregate, so a consumer does not need to make additional calls. This can be useful in situations where subscribers need to maintain a synchronized view of the data. Compacted keyed topics typically use this style of messages.

1{
2  "headers": {
3    "event-id": "",
4    "aggregate-id": "1",
5    "aggregate-type": "customer"
6  },
7  "payload": {
8    "id": 1,
9    "firstName": "string",
10    "lastName": "string",
11    "password": "string",
12    "email": "string",
13    "username": "string",
14    "address": {
15      "id": 1,
16      "street": "string",
17      "city": "string",
18      "state": "string",
19      "zip": "string"
20    }
21  }
22}

Domain Event Messages

Domain Event Messages contains information about the event and interesting portions of the underlying aggregate, but not the entire state of the aggregate. This style of events is typically used for Event Sourcing integration patterns.

1{
2  "headers": {
3    "event-type": "customer-address-updated",
4    "event-id": "",
5    "aggregate-id": "1",
6    "aggregate-type": "customer"
7  },
8  "payload": {
9    "id": 1,
10    "eventType": "address-updated",
11    "customer": {
12      "id": 1,
13      "new-address": {
14        "street": "string",
15        "city": "string",
16        "state": "string",
17        "zip": "string"
18      }
19    }
20  }
21}

Next: Java Code Generator for AsyncAPI

Next: Java Code Generator for AsyncAPI


Originally published at https://zenwave360.github.io