spec icon indicating copy to clipboard operation
spec copied to clipboard

Possibility to Describe Channel Ownership

Open benjaminknauer opened this issue 8 months ago • 7 comments

Problem Statement: Channel Ownership

In a distributed architecture, applications typically own channels and define a contract for using those channels. For example, an application might define an outgoing channel and publish events on that channel. The data model of the messages is then defined by that application's team. The owner of such a channel appears as the sender in the AsyncAPI. However, an application can also define incoming channels, for example when using the command pattern. In this case, the structure of the messages is defined by the application acting as the receiver.

Ways to Use AsyncAPI

AsyncAPI provides means to describe an application’s asynchronous communication. In my view, it is not precisely specified whether:

  1. An application’s AsyncAPI should only include the channels that the application owns - that is, the channels to which the application publishes events and from which it receives commands - or
  2. The AsyncAPI should represent the entire asynchronous communication of the application.

Tools that generate AsyncAPI from source code (such as Springwolf) automatically include every discoverable channel in the AsyncAPI. They therefore follow approach 2 by default.

Problem

If an application’s AsyncAPI includes all channels - even those not owned by the application - it is impossible to distinguish in the AsyncAPI which channels are defined by the application and which are merely consumed. This has implications for using the AsyncAPI.

In our ongoing insurance project client code is generated from the full AsyncAPI spec - so even channels we merely consume end up in our generated SDKs. This should be a pretty typical use case.

Client code should only be generated for the channels/messages genuinely owned by the application. AsyncAPI 3.0, however, does not currently support this distinction. We initially considered using tags to mark channels, but tags in AsyncAPI are primarily intended for documentation grouping and carry no standardized semantic meaning. Tool support for tags is inconsistent - relying on them would force us to build and maintain custom filtering logic - and they lack the granularity and structure.

Proposal: Channel Ownership

To solve the problem described above, the concept of ownership could be introduced into AsyncAPI. This is most relevant for channels, but could also be applied to other AsyncAPI objects. By explicitly annotating each channel (or object) with its ownership status, tooling can automatically filter out external dependencies and generate client code only for those definitions truly maintained by the service. In addition, this metadata strengthens governance and traceability, as it clearly identifies which team is responsible for evolving and supporting each part of the asynchronous contract.

Introducing an ownership Field on channel Objects

Below is a first, rough sketch of how an ownership concept could be modeled in AsyncAPI. It serves as an initial proposal and can be refined further to cover additional use cases and objects.

Table of the ownership Attribute

Field Type Required? Description
mode string yes Indicates whether the service owns the channel (owner) or follows the channel (follower).
source object no (only allowed if mode=follower) Metadata about the application that owns the channel.
source.id string (urn) yes (within source) The AsyncAPI ID of the application that actually owns the channel.
source.version string no Version of the owner's specification on which this follower is based.
source.url string (url) no URL to the original AsyncAPI document of the owning application.
source.contact object no Contact details of the team owning the channel.
source.contact.email email yes (within contact) Contact person or team email address in the owning service.

Note: All source.* fields are valid only if mode=follower.

Example Usage of the ownership Attribute

asyncapi: '3.0.0'
id: urn:asyncapi:team-a:product-service
info:
  title: ProductService
  version: '1.0.0'

channels:
  # This service owns the channel and publishes events here
  productCreated:
    ownership:
      mode: owner

  # This service follows the channel defined by OrderService
  orderPlaced:
    ownership:
      mode: follower
      source: # only permitted if mode=follower
        id: urn:asyncapi:team-b:order-service
        version: '2.1.0'
        url: 'https://example.com/specs/order-service.yaml'
        contact:
          email: '[email protected]'

Schema Definition of the ownership Attribute

$schema: "http://json-schema.org/draft-07/schema#"
title: "AsyncAPI Channel Ownership Validation"
description: >
  Optionally validates the `ownership` object for all channels in an AsyncAPI spec.
type: object

properties:
  channels:
    type: object
    description: Collection of all channel definitions
    additionalProperties:
      type: object
      description: Individual channel entry
      properties:
        ownership:
          $ref: "#/components/schemas/Ownership"

components:
  schemas:
    OwnershipSource:
      type: object
      description: Metadata about the channel owner (only for followers)
      properties:
        id:
          type: string
          description: AsyncAPI ID of the owning application
        version:
          type: string
          description: Version of the owner specification
        url:
          type: string
          format: uri
          description: URL to the original AsyncAPI document
        contact:
          type: object
          description: Contact details of the owning team
          properties:
            email:
              type: string
              format: email
              description: Email address of the contact in the owner service
          required:
            - email
      required:
        - id

    Ownership:
      type: object
      description: Ownership definition (optional)
      properties:
        mode:
          type: string
          enum:
            - owner
            - follower
        source:
          $ref: "#/components/schemas/OwnershipSource"
      required:
        - mode
      oneOf:
        # owner: forbid any source
        - description: Owner variant (no source allowed)
          properties:
            mode:
              const: owner
          required:
            - mode
          not:
            required: [ source ]

        # follower: source is optional
        - description: Follower variant (source optional)
          properties:
            mode:
              const: follower
          required:
            - mode

Many thanks to @danielkocot for talking this through with me beforehand.

benjaminknauer avatar May 13 '25 12:05 benjaminknauer

Welcome to AsyncAPI. Thanks a lot for reporting your first issue. Please check out our contributors guide and the instructions about a basic recommended setup useful for opening a pull request.
Keep in mind there are also other channels you can use to interact with AsyncAPI community. For more details check out this issue.

github-actions[bot] avatar May 13 '25 12:05 github-actions[bot]

Hey there, thanks for putting it all together.

  1. The AsyncAPI should represent the entire asynchronous communication of the application.

Spec in the intro says: The AsyncAPI document SHOULD describe the operations an application performs.

This means that all channels related to these operations should be included in the AsyncAPI document. Not only the owned ones.

It also says The AsyncAPI specification does not assume any kind of software topology, architecture or pattern.

To be honest, I think you need to bring a bit more info to the table. When I read In our ongoing insurance project client code is generated from the full AsyncAPI spec - so even channels we merely consume end up in our generated SDKs. This should be a pretty typical use case. I don't think I fully understand:

  • What does it actually mean to own, are you producing/consuming messages only from channels you own?
  • The AsyncAPI should represent the entire asynchronous communication of the application - my answer to this is yes, but I'm confused, as later you write:
    • so even channels we merely consume - so in the end, you need them if you use them, so why shouldn't they be in the SDK?
    • the full AsyncAPI spec - what is this document actually representing, the app or the whole system? I'm basically confused how an app can contain channels and operations it doesn't need 🤔 especially if the AsyncAPI document is generated from code - so how can the code relate to functionality that is not needed?

Sorry if some basic questions were asked, but I'm not familiar with Springwolf and, in general, not using code-first tools - not my favourites.

And can you write a paragraph or more about the use case for the new ownership object? How code-first tools and SDK generators will benefit from this?

Like for example, would you use it to generate more than one AsyncAPI document, and use it for client generation, the one you need, and if later in docs you need to share the entire communication, you bundle these files in one?

derberg avatar May 15 '25 11:05 derberg

Hi @derberg,

Thanks for your quick feedback and questions. I’ll try to clarify the key points. I'm happy to answer follow-up questions after my two-week vacation 😊


OpenAPI vs. AsyncAPI (“Application Interface” approach)

  • OpenAPI describes only the REST endpoints that a consumer can call on a service. Outgoing calls that the service makes internally are not documented in the OpenAPI spec.
  • AsyncAPI, on the other hand - especially in our “Approach 2” - documents all asynchronous channels that an application reads from or writes to. That’s akin to having an OpenAPI spec include every internal REST call the service makes.

Our goal, however, is for the AsyncAPI spec in our API portal to include only the channels that external teams can actually use:

  1. Commands (incoming): channels on which the service receives commands.
  2. Events (outgoing): channels on which the service publishes events.

The application's purely internal channels should not be part of the publicly visible spec.


Our Specific Use Case

We run an enterprise-wide API portal that catalogs both REST and asynchronous APIs, giving teams a single source of truth for how to interact with any service.

When Team X needs to integrate with Service Y, they discover Y’s AsyncAPI in our portal and generate client libraries directly from that spec. However, because our portal ingests the full AsyncAPI document - including every internal channel that Y uses to communicate with other microservices - our code generator currently emits message classes for channels that are not part of Y’s public API.


Why Exclude “Follower” Channels from Generated SDKs?

  1. Unnecessary Code
    Generating message classes for every consumed channel bloats the client SDK with types that should not be sent or processed.
  2. Risk of Contract Drift
    If Team B (the channel owner) evolves the contract to v1.1 but Service A’s spec is still on v1.0, generating a new SDK for Team C from A’s spec yields stale message definitions.
  3. Clarity in the API Portal
    Consumers want a clear list of operations they can perform on the service, not a full map of internal asynchronous wiring.

What Does “Ownership” Mean?

  • Owner Channel: Your service defines and maintains the channel contract (payload schema, message semantics).
  • Follower Channel: Your service only consumes a channel that is maintained by another service.

By explicitly marking each channel with an ownership attribute, tooling and portals can automatically filter which channels truly belong to this service, and which are external dependencies.


How Code-First Tools and SDK Generators Could Benefit from ownership

  • How do code-first tools fit in?
    Tools like Springwolf will continue generating AsyncAPI from source code, but could respect annotations for the ownership set by the developers to mark a channel. 
  • Automatic Filtering in Code Generators
    Generators read ownership.mode. Only channels with mode: owner are included in the generated client code.
  • Possibility to Split Specs
    • Public API Spec: contains only owner channels (for SDK generation and portal catalog).
    • Full System Spec: bundles owner + follower channels for internal architecture docs.
  • Governance & Traceability
    Each follower channel links back to its true owner via source.idsource.url, and source.version, making ownership clear and easing change management.

In summary, introducing a standardized ownership field in AsyncAPI offers a clear separation between public interfaces and internal communication - preventing unnecessary code generation, reducing compatibility risks, and delivering a streamlined, consumer-focused view in our API portal.

Alternatively, we could approach this problem from an entirely different perspective. I’m eager to explore other possible solutions.

I hope this (hopefully) sharper wrap-up helps!

benjaminknauer avatar May 16 '25 19:05 benjaminknauer

Thanks for more details.

documents all asynchronous channels that an application reads from or writes to

AsyncAPI does not have a strong requirement that you have one AsyncAPI document and that it documents ALL.

The AsyncAPI document SHOULD describe the operations an applicationp performs - no strong MUST and no explicit requirement that ALL communication must be there. And again, it says The AsyncAPI specification does not assume any kind of software topology, architecture or pattern.

Nothing blocks you from having an AsyncAPI document, actually multiple documents, one for internal and one for external (so they can generate client) and later to bundle them and use to generate client for the app itself.

derberg avatar May 19 '25 12:05 derberg

I'd like to check my understanding of the proposal, please.

Consider application A that uses Kafka topic PROCESSING. It subscribes to the PROCESSING topic. It consumes events from the PROCESSING topic. The owner of application A writes an AsyncAPI document so other developers can write other applications to send events for application A to process.

Does application A own the PROCESSING topic?

Consider application B that uses Kafka topic ORDERS. Application B is a message producer. It publishes events to the ORDERS topic when orders are made. The owner of application B writes an AsyncAPI document so other developers can write applications to receive notifications from the ORDERS topic.

Does application B own the ORDERS topic?

My understanding of your proposal is that the answer is "yes" to both of these examples - that ownership isn't defined by whether you are a producer or consumer, but rather by who gets to decide on the way the topic is used, the message schema that should be adopted, etc.

Am I correct?

dalelane avatar May 21 '25 17:05 dalelane

Thanks a lot for taking the time to look into this!


@derberg - you’re right: splitting the spec into one public and one internal document will solve our immediate problem, and that’s exactly what we’re preparing to do. We still wanted to raise the topic here because we believe the underlying need - separating what I own from what I merely use - is common enough to deserve first-class support in the spec.

Beyond code generation, an explicit ownership (or similar) field would also help in other areas. For example:

  • Governance & traceability: central API portals (like ours) could track which team owns which contract. A machine-readable owner/follower link prevents orphaned channels and makes breaking-change audits easier.
  • Change management: validation, linting, dependency graphs and automatic change notifications all could benefit from a clear ownership attribute.

We drafted this proposal after a Slack chat between @danielkocot and @fmvilas, where @fmvilas mentioned that other teams had reached out to him with similar challenges. @fmvilas - could you perhaps elaborate on the specific use cases those other teams were facing?


@dalelane - your understanding is exactly what we had in mind. Ownership is about who defines the contract (schema, semantics, evolution), not about producer vs. consumer. In your examples:

  • Application A / PROCESSING topic: A owns the topic because it decides the event shape it wants to receive.
  • Application B / ORDERS topic: B owns the topic because it sets the schema other services must follow.

benjaminknauer avatar Jun 03 '25 07:06 benjaminknauer

It's always around governance, understanding which teams are in charge of which channels. However, I often point people to EventCatalog as a solution for that. It leverages AsyncAPI too. I'm not quite sure this is something that should concern AsyncAPI but I'm also not against it.

fmvilas avatar Sep 02 '25 17:09 fmvilas