Events and asynchronous messaging
This chapter specifies how to represent:
-
any type of message that is exchanged using asynchronous messaging
-
messages that communicate an event, even if exchanged synchronously
Asynchronous messaging refers to communication where participants don’t need to be actively present or available at the same time to exchange information. It’s characterized by messages being sent and received at different times, without the expectation of an immediate response.
Asynchronous messaging is enabled by a message broker between the message sender (producer) and receiver (consumer). In the broad sense, a message broker can be any system that decouples both parties e.g. a Kafka broker, an AMQP-compatible broker, or a message database exposed using a REST API or webhooks.
In general, we can distinguish following types of messages:
| message type | conveys | properties |
|---|---|---|
Command |
a request for action |
|
Query |
a request for information |
|
Event |
a data record expressing an occurrence and its context (*) |
(*) from the CloudEvents Specification |
All of these message types can be communicated synchronously or asynchronously, though commands and queries are exchanged often synchronously while events typically delivered asynchronously.
To represent asynchronous messages in an interoperable way, this guide promotes and adopts the CloudEvents specification which is part of the Cloud Native Computing Foundation (CNCF) and had its 1.0 release in 2019. This guide extends upon this specification, defining additional guidelines and context attributes. While the CloudEvents specification only focuses on messages of type event, this guide also extends it to add support to represent commands and queries for asynchronous messaging (e.g. an asynchronous request-reply exchange).
The CloudEvents specification thoroughly separates the core conceptual event model from any format and protocol details. It standardizes bindings to common protocols like HTTP, Kafka, AMQP and MQTT. Moreover, the project provides SDKs for most popular programming languages to support integration of CloudEvents.
| Description | Link |
|---|---|
CloudEvents overview |
|
Conceptual model |
https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md |
JSON Event Format |
https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/formats/json-format.md |
HTTP Protocol Binding |
https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/bindings/http-protocol-binding.md |
Web Hooks for Event Delivery |
https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/http-webhook.md |
Extensions (Sequence, Claim Check, Partitioning, …) |
https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/documented-extensions.md |
Rule: CloudEvents format for asynchronous messages [async-cespec]
The semantics, formats and bindings of an Asynchronous Message, an extension of the CloudEvents Specification v1.0, SHOULD be used to represent events and asynchronous messages in APIs.
An Asynchronous Message extends a CloudEvent to describe not only events, but any type of asynchronous message (e.g. commands or queries both with request and reply) using a CloudEvent-compatible format. It also specifies additional guidelines and context attributes.
Asynchronous Messages contain two types of information:
-
context : Metadata providing contextual information about the message
-
data : Domain-specific information about the message (i.e. the payload). This might include information about an event occurrence, details about the data that was changed, or more.
When using the CloudEvent structured JSON format, the data is represented by a data (any JSON type) or data_base64 property (a binary as base64-encoded string).
All functional message data SHOULD be part of an Asynchronous Message, without relying on additional protocol-specific information. Protocol-specific headers can be used for non-functional aspects like authentication and authorization, routing or tracing. Routing can be performed based on information contained in the message, but a message SHOULD NOT identify a specific routing destination.
An Asynchronous Message MUST be immutable. If a context attribute is added by an intermediary, the result is considered a new Asynchronous Message and the identifier of the message (combination of source and id) should be changed as well. Exchanging a message multiple times over different protocol bindings doesn’t impact its identity.
Rule: Context attributes of an asynchronous message [async-ctxt]
The context of an Asynchronous Message is represented by attributes that MUST follow strict naming conventions and type system.
The table below lists context attributes standardized by the CloudEvents Specification, alongside the attributes service, relatedto and relatedtosource of an Asynchronous Message defined by this guide.
Future minor version updates of the CloudEvents specification or of the Asynchronous Message schemas may define additional attributes that may safely be ignored for existing clients. For forward-compatibility, consumers SHOULD ignore any unknown context attributes. As an Asynchronous Message is immutable ([async-cespec]), if the message is redistributed to other consumers with the same id and source values, all attributes, including the unknown ones, MUST remain unchanged.
| context attribute | format | example | description |
|---|---|---|---|
|
REQUIRED string |
"1.0" |
The version of the CloudEvents specification used to represent the message. Producers MUST use 1.0 to reference this version of the specification, while consumers SHOULD accept any value with major version number 1. |
|
REQUIRED string see [async-svctype] |
|
Type of the message. Often this attribute is used for routing, observability, policy enforcement, etc. |
|
REQUIRED uri-reference (string) absolute URI recommended |
|
identifies the source of the message. The exact syntax and semantics behind the data encoded in the URI is defined by the API. |
|
REQUIRED string (UUID recommended) |
|
Identifies the message. Producers MUST ensure that |
|
OPTIONAL URI (string) |
|
Generally, the API definition is used which can be found based on |
|
OPTIONAL date-time (string) |
|
Timestamp of when the occurrence (in case of an event) happened. If the time of the occurrence cannot be determined then this attribute MAY be set to some other time (such as the current time) by the CloudEvents producer, however all producers for the same source of a single |
|
OPTIONAL string |
|
Media type of data. For CloudEvent JSON format, defaults to |
|
OPTIONAL string |
Identifier of the resource that’s the subject of the message, e.g. for which an event occurred or a request is sent. The subject may also be modeled as body property to represent more complex identifiers (e.g. composite parent/child id), to enforce a more specific format ( |
|
|
REQUIRED string see [async-svctype] |
|
Identifies the API description defining the event types.
It can be used to perform more coarse grained routing of messages, rather than on individual |
|
REQUIRED for reply messages only string |
the value of |
|
|
REQUIRED for reply messages only string |
the value of |
Rule: Value of service and type of an asynchronous message [async-svctype]
The value of the service and type context attributes SHOULD be structured as follows:
| context attribute | format |
|---|---|
|
serviceName |
|
serviceName |
with:
serviceName |
The API which defines the message. It SHOULD be prefixed with a reverse-DNS name. The prefixed domain dictates the organization which defines the semantics of the message type. |
majorVersion |
The major version of the API |
method |
For events: a past participle indicating the action or change that occurred e.g. "created", "updated", "deleted", … For queries: For commands: |
suffix |
For commands and queries:
Absent for events. |
resourceType childResourceType |
The type of resource of the subject of the message. The same names and resource hierarchy SHOULD be used consistently across REST APIs (see Document) and asynchronous messages, i.e. a lowerCamelCase noun in American English. Use a singular noun if the message applies on a single entity, and plural if it applies to multiple or a variable number of entities. For child resource types, add one or more childResourceType segments. |
| To reduce complexity of managing multiple versions, versioning is done at service-level, not for each specific event type. This is similar to REST APIs, where versions are also managed at API-level rather than at operation-level. |
service and type values| attribute | value |
|---|---|
|
|
|
|
|
|
|
|
The context attributes specversion, id, source and type are mandatory according to the CloudEvents specification. If datacontenttype is absent, it is to be considered as application/json.
data_base64 is to be used instead of data in case of a binary payload.
JSON Event Format for CloudEvents provides the full specification on how CloudEvents are represented in JSON.
{
"specversion": "1.0",
"id": "450e8400-e29b-41d4-a716-446655440007",
"service": "be.belgif.example.meeting.v3",
"type": "be.belgif.example.meeting.v3.meeting.invited",
"source": "urn:async-source:be.belgif.example",
"subject": "132456",
"data": {
"id": "132456",
"invitees": [ "123", "456" ],
"period": {
"start": "2025-10-06T13:30:00+02:00",
"end": "2025-10-06T14:30:00+02:00"
}
}
}
Rule: Problem replies for asynchronous operations [async-prbl]
Each asynchronous interaction (command or query) that provides a reply, SHOULD also provide a problem reply, with a Problem Details object (RFC 9457) as data. The same standardized Belgif problem types SHOULD be used as for REST APIs (see Error handling).
The type SHOULD be the same as the one of the request, but with the problemReply suffix instead of reply.
The AsynchronousProblemReplyMessage schema of the Belgif openapi-cloudevents reusable OpenAPI can be used.
If an API provides a reply when a received message has an unexpected service or type value, the urn:problem-type:belgif:resourceNotFound problem type SHOULD be used. If the received service was unexpected, the value of service in the problem reply SHOULD be the one identifying the API that received the message.
Rule: Size of an asynchronous message [async-size]
Implementations handling Asynchronous Messages MUST at least support messages up to 64kB. APIs SHOULD be designed not to exceed this limit. If an API allows to exchange larger messages, it MUST be explicitly stated in its documentation.
For use cases requiring large data exchanges, consider using the claim-check pattern i.e. include a reference to the large data instead.
If the claim-check pattern is used for the entire data of the Asynchronous Message, the dataref extension attribute SHOULD be used and documented in the API description.
Alternatively, the claim-check pattern can be used to only split off parts of the data. In this case, the reference(s) SHOULD be part of the structured value of the data attribute.
The following example shows a message in which the entire data is replaced by a reference:
{
"specversion": "1.0",
"type": "be.belgif.example.meeting.v3.meeting.get.reply",
"service": "be.belgif.example.meeting.v3",
"source": "urn:async-source:be.belgif.example",
"id": "b73b2241-d57a-4c9f-aa7e-d4eb611a3b5c",
"datacontenttype": "application/json",
"dataref": "https://api.example.org/demo/v1/meetings/456789"
}
The following example shows a message in which only parts of the data are replaced by a reference:
{
"specversion": "1.0",
"type": "be.belgif.example.meeting.v3.meeting.get.reply",
"service": "be.belgif.example.meeting.v3",
"source": "urn:async-source:be.belgif.example",
"id": "d1ef338f-a957-410a-8af7-657df6336be2",
"data": {
"id": "1234",
"name": "Board Meeting",
"attachments": [
"https://api.example.org/demo/v1/attachments/1",
"https://api.example.org/demo/v1/attachments/2"
]
}
}
Rule: Specifying asynchronous messages [async-contract]
Related asynchronous message types and their exchanges SHOULD be grouped together in a service, which is documented by an OpenAPI 3.0 or AsyncAPI 3.0 description. This provides more cohesive documentation and makes it easier to manage the lifecycle of asynchronous message types, similar as how synchronous calls are grouped and documented together for a REST API.
OpenAPI can be used when asynchronous messages are transferred over HTTP(S), while AsyncAPI also allows for other exchange protocols.
When using AsyncAPI, schemas of the events SHOULD be put in (a) separate schema-only OpenAPI document(s) and referenced from the AsyncAPI document. This allows the use of OpenAPI tooling like code generation and editing for message schemas. The same guidelines as for REST APIs apply, like naming conventions, reuse of components and versioning.
The Belgif openapi-cloudevents reusable OpenAPI provides schemas for asynchronous messages the structured JSON CloudEvent format.
You can extend the AsynchronousMessageBase or AsynchronousReplyMessageBase schemas using allOf to define schemas for specific message types. AsynchronousProblemReplyMessage can be used for problemReply messages.
There SHOULD be a single schema for each event type value, though a schema may be shared by multiple types.
Rule: Endpoints for transfer of asynchronous messages of any type [async-generic]
When providing a generic asynchronous endpoint for exchange of message types of multiple unrelated business domains, you can use the generic AsynchronousMessage schema of Belgif openapi-cloudevents. The definitions of specific message types should still be grouped together in a service and documented in another AsyncAPI or OpenAPI description, by extending the AsynchronousMessageBase schema (see [async-contract]).
For batch transfers supporting a very large number of messages, like SFTP file transfer, the AsynchronousMessagesBatch schema SHOULD be used.
Additional data outside the AsynchronousMessage MUST NOT be required for the functional processsing of a message, but MAY support technical aspects during message transfer like batch-level routing or authentication.
The following documents describe an example meeting API that allows to manage meetings and get notifications upon meeting changes over two protocols: using a REST API and using JMS messaging.
OpenAPI with only the message schemas |
|
OpenAPI documenting asynchronous message exchange over REST API referencing schemas from meeting-messages.yaml |
|
AsyncAPI documenting message exchange over JMS message broker referencing schemas from meeting-messages.yaml |
components:
schemas:
SentMeetingMessage:
description: Schema combining all async messages that can be sent in the meeting API
type: object
oneOf:
- $ref: "#/components/schemas/CreateMeetingRequestMessage"
discriminator:
propertyName: type
mapping:
"be.belgif.example.meeting.v3.meeting.create.request": "#/components/schemas/CreateMeetingRequestMessage"
ReceivedMeetingMessage:
description: Schema combining all async messages that can be received in the meeting API
type: object
oneOf:
- $ref: "#/components/schemas/CreateMeetingReplyMessage"
- $ref: "#/components/schemas/CreateMeetingProblemReplyMessage"
- $ref: "#/components/schemas/MeetingInvitedMessage"
- $ref: "#/components/schemas/MeetingCanceledMessage"
discriminator:
propertyName: type
mapping:
"be.belgif.example.meeting.v3.meeting.create.reply": "#/components/schemas/CreateMeetingReplyMessage"
"be.belgif.example.meeting.v3.meeting.create.problemReply": "#/components/schemas/CreateMeetingProblemReplyMessage"
"be.belgif.example.meeting.v3.meeting.invited": "#/components/schemas/MeetingInvitedMessage"
"be.belgif.example.meeting.v3.meeting.canceled": "#/components/schemas/MeetingCanceledMessage"
CreateMeetingRequestMessage:
type: object
allOf:
- $ref: "#/components/schemas/MeetingMessage"
properties:
data:
$ref: "#/components/schemas/Meeting"
required: [data]
CreateMeetingReplyMessage:
type: object
allOf:
- $ref: "#/components/schemas/MeetingMessage"
# extends reply message, which adds relatedto(source) properties
- $ref: "./belgif/cloudevents/v1/cloudevents-v1.yaml#/components/schemas/AsynchronousReplyMessageBase"
properties:
data:
type: object
properties:
id:
type: string
required: [id]
required: [ data ]
CreateMeetingProblemReplyMessage:
type: object
allOf:
- $ref: "#/components/schemas/MeetingMessage"
- $ref: "./belgif/cloudevents/v1/cloudevents-v1.yaml#/components/schemas/AsynchronousProblemReplyMessage"
MeetingInvitedMessage:
type: object
allOf:
- $ref: "#/components/schemas/MeetingMessage"
properties:
data:
$ref: "#/components/schemas/Meeting"
required: [ data ]
MeetingCanceledMessage:
type: object
allOf:
- $ref: "./belgif/cloudevents/v1/cloudevents-v1.yaml#/components/schemas/AsynchronousMessageBase"
properties:
data:
type: object
properties:
id:
description: Identifier of the meeting
type: string
required: [id]
required: [ data ]
In AsyncAPI, commands and queries with replies can be represented as operations:
operations:
createMeeting:
summary: "Create a meeting"
action: send
channel:
$ref: '#/channels/MeetingSendChannel'
messages:
- $ref: '#/channels/MeetingSendChannel/messages/be.belgif.example.meeting.v3.meeting.create.request'
reply:
channel:
$ref: '#/channels/MeetingReceiveChannel'
messages:
- $ref: '#/channels/MeetingReceiveChannel/messages/be.belgif.example.meeting.v3.meeting.create.reply'
- $ref: '#/channels/MeetingReceiveChannel/messages/be.belgif.example.meeting.v3.meeting.create.problemReply'
Note that it is also possible to define operations with action: receive, where the API client receives requests and sends replies.
Rule: Duplicates and out-of-order messages [async-order]
Distributed and concurrent systems make it hard to guarantee exactly-once or order-preserving message delivery. Consumers SHOULD NOT rely on messages being unique and sent in (chronological) order unless explicitly documented in the API description.
Asynchronous messages SHOULD be designed to allow duplicates and out-of-order processing. A consumer should be able to detect duplicates or any ordered relationship without having to rely on the order of arrival or transport-layer information not part of the Asynchronous Message.
While the combination of source and id is unique, this might not be sufficient e.g. in a migration scenario to detect duplicates when the same data change event is provided in multiple messages in different formats. A separate identifier of the data change may be needed in such case.
As common example, idempotent out-of-order processing for data change events can be supported by sending the following information:
-
the document (entity) identifier,
-
an ordering key that increases monotonically for this document
-
the document state after the change
A consumer interested in the current state can then ignore events older than the last processed event of the document. A consumer interested in the history of a document can use the ordering key to recreate a (partially) ordered sequence of events, and ignore duplicate events with the same ordering key. If a consultation operation for the document exists, including the same ordering key in its response allows consumers to ignore any data change events that were already applied.
Note: timestamps are often a bad choice as ordering key, since in distributed systems events may occur at the same time, and clocks are not sufficiently synchronized, and jump forward and backward to compensate drifts or leap seconds.
This example represents two events for a name change on the same person.
ssin is the document identifier, while version increases sequentially for any change on the person. The event includes the full representation of the person after the change.
{
"specversion": "1.0",
"id": "450e8400-e29b-41d4-a716-446655440007",
"service": "be.belgif.example.person.v3",
"type": "be.belgif.example.person.v3.person.nameChanged",
"source": "urn:async-source:be.belgif.example",
"data": {
"ssin": "12345678901",
"version": "2",
"name": {
"givenName": "John",
"familyName": "Doe"
}
}
}
{
"specversion": "1.0",
"id": "550e8400-e29b-41d4-a716-446655440009",
"service": "be.belgif.example.person.v3",
"type": "be.belgif.example.person.v3.person.nameChanged",
"source": "urn:async-source:be.belgif.example",
"data": {
"ssin": "12345678901",
"version": "3",
"name": {
"givenName": "Johnny",
"familyName": "Doe"
}
}
}