Understanding AsyncAPI's publish & subscribe semantics with an example


In AsyncAPI, a channel can have a publish and subscribe operation. This can be confusing, depending on which perspective you’re considering (server vs. user) and what you’re comparing against (eg. WebSocket).

In this blog post, I want to go through an example to show you how to construct your AsyncAPI file with the right publish and subscribe semantics. As a bonus, I also show you how to refactor your AsyncAPI files with common configuration.

Publish vs. Subscribe semantics in AsyncAPI

Before we look into an example, here’s a table that explains what publish and subscribe mean in AsyncAPI and how it related to other similar concepts such as WebSocket’s send and receive.

AsyncAPI Term WebSocket Term From Server Perspective From User Perspective
Publish Send The channel receives the message User publishes messages to the channel
Subscribe Receive The channel publishes the message User subscribes for messages from the channel

In my experience, it’s easier to think of publish and subscribe from user’s perspective:

  • A user publishes messages to a channel on a server.
  • A user subscribes for messages from the channel on a server.

Demystifying the Semantics of Publish and Subscribe blog post goes into more detail into this, if you want to read more.

Account and Email Services

Now that we understand the basic semantics of publish and subscribe, let’s look into an example outlined in the excellent Understanding AsyncAPIs with a Practical Example blog post to see if we can construct AsyncAPI definitions with the proper semantics.

Imagine you want to describe two microservices: Account Service emits an userSignedUp event and Email Service receives that event:

Account and Email services

How do you define such an architecture in AsyncAPI?

Account Service: AsyncAPI definition

Let’s look at the Account Service first.

First, you need to define some metadata and the server:

asyncapi: '2.6.0'
info:
  title: Account Service
  version: 1.0.0
  description: Processes user sign ups and publishes an event afterwards

servers:
  test:
    url: mqtt://test.mosquitto.org
    protocol: mqtt
    description: Test MQTT broker

Next, you need to define a channel for the server with subscribe or publish operation. At this point, you need to ask yourself as a user: Do I publish messages to or subscribe messages from this channel? It’s the latter, because the service emits userSignedUp, so the channel needs to have subscribe operation:

channels:
  user/signedup:
    subscribe:
      operationId: publishUserSignedUp
      message:
        $ref: '#/components/messages/userSignedUp'

You can also see that the channel refers to a userSignedUp message. Let’s define that with a simple schema as well:

components:
  messages:
    userSignedUp:
      name: userSignedUp
      title: User signed up message
      summary: Emitted when a user signs up
      contentType: application/json
      payload:
        $ref: '#/components/schemas/userSignedUpPayload'

  schemas:
    userSignedUpPayload:
      type: object
      properties:
        displayName:
          type: string
          description: Name of the user
        email:
          type: string
          format: email
          description: Email of the user

account-service.yaml

Email Service: AsyncAPI definition

In Email Service, you need to ask the same question: Do I publish messages to or subscribe messages from this channel?. The answer is the former, because the service receives messages, so the channel needs to have the publish operation:

channels:
  user/signedup:
    publish:
      operationId: receiveUserSignedUp
      message:
        $ref : '#/components/messages/userSignedUp'

The rest of the email-service.yaml is identical to account-service.yaml.

Refactoring AsyncAPI definitions

As you’ve seen, 80% of email-service.yaml and account-service.yaml are identical. Can we somehow refactor these? Yes we can!

We can put together the common parts of these two definitions (such as server, message, schema) into a common.yaml:

# Common AsyncAPI config shared between Account and Email Services
servers:
  test:
    url: mqtt://test.mosquitto.org
    protocol: mqtt
    description: Test MQTT broker

components:
  messages:
    userSignedUp:
      name: userSignedUp
      title: User signed up message
      summary: Emitted when a user signs up
      contentType: application/json
      payload:
        $ref: '#/components/schemas/userSignedUpPayload'

  schemas:
    userSignedUpPayload:
      type: object
      properties:
        displayName:
          type: string
          description: Name of the user
        email:
          type: string
          format: email
          description: Email of the user

Then, in account-service-common.yaml, you can simply refer to the common server and message configuration:

# Same as account-service.yaml except it uses common.yaml to share common config
# between email-service.yaml
asyncapi: '2.6.0'
info:
  title: Account Service
  version: 1.0.0
  description: Processes user sign ups and publishes an event afterwards

servers:
  $ref: 'https://raw.githubusercontent.com/meteatamel/asyncapi-basics/main/samples/account-email-services/common.yaml#/servers'

channels:
  user/signedup:
    subscribe:
      operationId: publishUserSignedUp
      message:
        $ref: 'https://raw.githubusercontent.com/meteatamel/asyncapi-basics/main/samples/account-email-services/common.yaml#/components/messages/userSignedUp'

Of course, you can do the same in email-service-common.yaml.


In this blog post, I tried to explain the publish and subscribe semantics of AsyncAPI with a simple example and also showed you how to refactor AsyncAPI definitions with common configuration. There are two excellent blog posts already published by the AsyncAPI community on this topic that I relied on, if you want to read more:

In the next blog post, I will show how how to combine the two of my favorite open-source specifications: CloudEvents and AsyncAPI.


See also