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:
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
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:
- Understanding AsyncAPIs with a Practical Example
- Demystifying the Semantics of Publish and Subscribe
In the next blog post, I will show how how to combine the two of my favorite open-source specifications: CloudEvents and AsyncAPI.