Kontakt
stefan.bente[at]th-koeln.de
+49 2261 8196 6367
Discord Server
Prof. Bente Personal Zoom
Adresse
Steinmüllerallee 1
51643 Gummersbach
Raum 3219
Sprechstunde nach Vereinbarung
Terminanfrage: calendly.com Wenn Sie dieses Tool nicht nutzen wollen, schicken Sie eine Mail und ich weise Ihnen einen Termin zu.

In a system whose services work together through events, the events are the public side of a service. An event contract writes down what a service publishes, what its events are called and what data they carry, before the first producer or consumer is built. This page shows a contract in AsyncAPI using a bike sharing example and explains what belongs in an event and what does not.

Language
Deutsche Fassung
Example code repository
https://gitlab.com/archi-lab/public/learning/learn-event-driven-architecture/-/tree/main/asyncapi-event-contracts
Keywords
AsyncAPI, Event contract, Contract, Event-driven architecture, Microservices, Kafka, Channel, Message, Payload, Headers

Event contracts with AsyncAPI

English version. The German README.de.md is the source of truth and is the text embedded by the archi-lab.io infopage.

In a system whose services work together through events, the events are a service’s public interface. What a service publishes, what its events are called and which data they carry is what every consumer relies on. An event contract writes that down before the first producer or consumer is built. AsyncAPI is the format for it: AsyncAPI is to events what OpenAPI is to REST endpoints.

This module shows a contract on a small example and explains what belongs in an event and what does not.

The example: bike sharing

Customers take a bike from a station and return it to any station. Two services are involved:

Each service has its own contract, describing the service from its own point of view: what it sends and what it receives.

The building blocks of an AsyncAPI contract

A contract in AsyncAPI 3 has four parts.

  • info and servers: which service this is, in which version, and which broker carries its events. Here it is a locally started Kafka.
  • channels: where events flow. With Kafka, a channel’s address is the topic name. The rental service writes all events of a rental to a topic rentals.
  • operations: what the service does with a channel, send or receive. The rental service has two send operations, the billing service one receive operation on the same channel.
  • components: the messages themselves, with their headers, payload and Kafka key, and the schemas they are made of.

The message is the core. This is the payload of RentalEnded:

RentalEndedPayload:
  type: object
  required:
    - rentalId
    - bikeId
    - customerId
    - endStationId
    - startedAt
    - endedAt
  properties:
    rentalId:
      type: string
      format: uuid
    bikeId:
      type: string
    customerId:
      type: string
      format: uuid
    endStationId:
      type: string
    startedAt:
      type: string
      format: date-time
    endedAt:
      type: string
      format: date-time

What belongs in an event

Whether a contract is any good is decided less by its syntax than by three questions.

Does the event report a fact, or is it a call in disguise? An event describes something that has already happened, so it is named in the past tense: RentalEnded, not EndRental. An obvious mistake would be an event ChargeCustomer that the rental service sends to billing. That is a command wearing the shape of an event: the rental service would have to know that billing exists and what it should do, and the two services would be as tightly coupled as with a synchronous call. With RentalEnded the rental service only reports what happened. What follows from it, each consumer decides for itself.

Does it carry the data the consumers need, and no more? Billing needs to know who rode and for how long, so RentalEnded carries the customerId and both timestamps. The price is deliberately not in it: billing owns the tariff, and when it changes, the rental service should not notice. The customer’s name and address are missing too. Every field in a payload is a promise the producer cannot take back without asking its consumers.

Is a consumer missing? A contract is checked against everyone who needs the event. If maintenance were added later, counting how often a bike has been ridden, it could consume RentalEnded without the rental service changing anything. That is the real gain of events: new consumers cost the producer nothing, as long as the event already carries what they need.

One definition per event

The billing service does not define RentalEnded again. Its contract references the message in the rental service’s contract:

messages:
  RentalEnded:
    $ref: './rental-service.asyncapi.yaml#/components/messages/RentalEnded'

So every event has exactly one definition, and it lives with the service that owns the event. Put the contracts of all services side by side, and they become the system’s event catalogue.

Checking a contract

A contract is only a foundation once it is formally valid. Two ways:

  • In the browser: open the file in AsyncAPI Studio. Studio shows errors immediately and renders the contract as readable documentation.
  • On the command line, with Node.js:

    npx @asyncapi/cli validate contracts/rental-service.asyncapi.yaml
    

The contracts in this module have been checked with the AsyncAPI CLI and are valid.

Fine print: one topic per event type, or one per rental?

Both events of a rental go to the same topic rentals, with the rentalId as the Kafka key. Kafka guarantees order only within a partition, and all events with the same key land in the same partition. So every consumer sees RentalStarted before RentalEnded of the same rental. With one topic per event type that guarantee would be lost: a consumer could see a rental end before it knows the rental began.

The price: a consumer that only needs RentalEnded reads RentalStarted too and ignores it. That is usually cheap. Rule of thumb: what has to arrive in order for the same thing belongs in one topic, keyed by that thing’s id.

Fine print: headers and payload

Business data goes in the payload, technical data in the headers. Which headers a message carries is fixed here by the Kafka helper library of the Microservice Dungeon, and the contract describes what it actually sends: among others an event_id, unique per event, and event_type and event_type_version, from which a consumer recognises which message it has. Kafka can deliver an event more than once; the event_id lets a consumer recognise that it has already processed it.

When something happened in the domain belongs in the payload (startedAt, endedAt). The header created_at only says when the event was written. The module service-development-3-with-infrastructure shows how the library uses the headers.