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 einem System, dessen Services über Events zusammenarbeiten, sind die Events die öffentliche Seite eines Service. Ein Event-Contract schreibt auf, was ein Service veröffentlicht, wie seine Events heißen und welche Daten sie tragen, bevor der erste Producer oder Consumer gebaut wird. Diese Seite zeigt einen Contract in AsyncAPI an einem Fahrradverleih und erklärt, was in ein Event gehört und was nicht.

Sprache
English version
Beispielcode-Repository
https://gitlab.com/archi-lab/public/learning/learn-event-driven-architecture/-/tree/main/asyncapi-event-contracts
Keywords
AsyncAPI, Event-Contract, Contract, Event-getriebene Architektur, Microservices, Kafka, Channel, Message, Payload, Headers, MEA

Event-Contracts mit AsyncAPI

In einem System, dessen Services über Events zusammenarbeiten, sind die Events das öffentliche Interface eines Service. Was ein Service veröffentlicht, wie seine Events heißen und welche Daten sie tragen, darauf verlassen sich alle, die sie konsumieren. Ein Event-Contract schreibt das auf, bevor der erste Producer oder Consumer gebaut wird. AsyncAPI ist dafür das Format: Was OpenAPI für REST-Endpoints ist, ist AsyncAPI für Events.

Dieses Modul zeigt einen Contract an einem kleinen Beispiel und erklärt, was in ein Event gehört und was nicht.

Das Beispiel: ein Fahrradverleih

Kund:innen leihen an einer Station ein Fahrrad aus und geben es an einer beliebigen Station zurück. Zwei Services sind beteiligt:

Jeder Service hat seinen eigenen Contract. Er beschreibt den Service aus dessen eigener Sicht: was er sendet und was er empfängt.

Die Bausteine eines AsyncAPI-Contracts

Ein Contract in AsyncAPI 3 hat vier Teile.

  • info und servers: Welcher Service ist das, in welcher Version, und über welchen Broker laufen seine Events. Hier ist es ein lokal gestartetes Kafka.
  • channels: Wohin Events fließen. Bei Kafka ist die address eines Channels der Name des Topics. Der Rental-Service schreibt alle Events einer Ausleihe in ein Topic rentals.
  • operations: Was der Service mit einem Channel tut, send oder receive. Der Rental-Service hat zwei send-Operations, der Billing-Service eine receive-Operation auf demselben Channel.
  • components: Die Messages selbst, mit ihren Headers, ihrem Payload und ihrem Kafka-Key, und die Schemas, aus denen sie bestehen.

Der Kern ist die Message. So sieht der Payload von RentalEnded aus:

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

Was in ein Event gehört

Ob ein Contract taugt, entscheidet sich weniger an der Syntax als an drei Fragen.

Meldet das Event eine Tatsache, oder ist es ein verstecktes Kommando? Ein Event beschreibt etwas, das schon passiert ist, und heißt deshalb in der Vergangenheitsform: RentalEnded, nicht EndRental. Ein naheliegender Fehler wäre ein Event ChargeCustomer, das der Rental-Service an die Abrechnung schickt. Das ist ein Befehl, der nur die Form eines Events hat: Der Rental-Service müsste wissen, dass es eine Abrechnung gibt und was sie tun soll, und die beiden Services wären so eng gekoppelt wie bei einem synchronen Aufruf. Mit RentalEnded meldet der Rental-Service nur, was passiert ist. Was daraus folgt, entscheidet jeder Consumer selbst.

Trägt es die Daten, die die Consumer brauchen, und nicht mehr? Die Abrechnung braucht, wer gefahren ist und wie lange. Deshalb trägt RentalEnded die customerId und beide Zeitpunkte. Der Preis steht bewusst nicht darin: Den Tarif kennt die Abrechnung, und wenn er sich ändert, soll der Rental-Service davon nichts merken. Auch Name und Adresse der Kund:in fehlen. Jedes Feld im Payload ist ein Versprechen, das der Producer nicht mehr zurücknehmen kann, ohne seine Consumer zu fragen.

Fehlt ein Consumer? Ein Contract wird gegen alle geprüft, die das Event brauchen. Würde später eine Wartung dazukommen, die zählt, wie oft ein Fahrrad gefahren ist, könnte sie RentalEnded konsumieren, ohne dass der Rental-Service etwas ändern muss. Das ist der eigentliche Gewinn von Events: Neue Consumer kosten den Producer nichts, solange das Event schon trägt, was sie brauchen.

Eine Definition pro Event

Der Billing-Service definiert RentalEnded nicht noch einmal. Sein Contract verweist auf die Message im Contract des Rental-Service:

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

So gibt es für jedes Event genau eine Definition, und sie liegt beim Service, dem das Event gehört. Legt man die Contracts aller Services nebeneinander, entsteht daraus der Event-Katalog des Systems.

Einen Contract prüfen

Ein Contract ist erst dann eine Grundlage, wenn er formal gültig ist. Zwei Wege:

  • Im Browser: Datei im AsyncAPI Studio öffnen. Das Studio zeigt Fehler sofort und rendert den Contract als lesbare Dokumentation.
  • Auf der Kommandozeile, mit Node.js:

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

Die Contracts in diesem Modul sind mit der AsyncAPI CLI geprüft und gültig.

Fine print: ein Topic pro Event-Typ oder eines pro Ausleihe?

Beide Events einer Ausleihe liegen im selben Topic rentals, mit der rentalId als Kafka-Key. Kafka garantiert eine Reihenfolge nur innerhalb einer Partition, und alle Events mit demselben Key landen in derselben Partition. So sieht jeder Consumer RentalStarted vor RentalEnded derselben Ausleihe. Mit einem eigenen Topic pro Event-Typ ginge diese Garantie verloren: Ein Consumer könnte das Ende einer Ausleihe sehen, bevor er ihren Beginn kennt.

Der Preis dafür: Ein Consumer, der nur RentalEnded braucht, liest RentalStarted mit und ignoriert es. Das ist meist billig. Die Faustregel: Was für denselben Gegenstand in Reihenfolge ankommen muss, gehört in ein Topic, mit dessen Id als Key.

Fine print: Headers und Payload

Die fachlichen Daten stehen im Payload, die technischen in den Headers. Welche Headers eine Message trägt, legt hier die Kafka-Hilfsbibliothek des Microservice Dungeon fest, und der Contract beschreibt, was sie tatsächlich sendet: unter anderem eine event_id, eindeutig pro Event, sowie event_type und event_type_version, an denen ein Consumer erkennt, welche Message er vor sich hat. Kafka kann ein Event mehr als einmal zustellen. An der event_id erkennt ein Consumer, dass er es schon verarbeitet hat.

Wann etwas in der Domäne passiert ist, gehört in den Payload (startedAt, endedAt). Der Header created_at sagt nur, wann das Event geschrieben wurde. Wie die Bibliothek die Headers benutzt, zeigt das Modul service-development-3-with-infrastructure.