Read in English · Leer en español · Lire en français
Abschnitt 6.5 von 6.8 · Mittelstufe · 30 Min. Lesezeit
Synchrone und asynchrone Kommunikation: REST, gRPC, GraphQL und Messaging
Videolektion
5 Min. · auf Englisch gesprochen, mit UntertitelnNur anhören (Audio)
Transkript lesen
Sobald ein System aufgeteilt ist, müssen seine Teile über ein Netzwerk kommunizieren, und wie sie das tun, prägt das System ebenso wie seine Aufteilung. In dieser Lektion lernen Sie die beiden Grundmodi, die wichtigsten Technologien und wie man langlebige APIs entwirft.
Services kommunizieren auf zwei Arten. Ein Telefonanruf ist synchron: Sie warten in der Leitung auf eine Antwort, und ist niemand da, bekommen Sie nichts. Ein Brief ist asynchron: Sie werfen ihn ein, gehen Ihrem Tag nach, und die Antwort kommt später. Telefonanrufe sind einfach, binden aber beide an denselben Moment. Briefe sind flexibler, aber Sie wissen nicht sofort, ob Ihr Anliegen Erfolg hatte. So oder so brauchen beide Seiten eine gemeinsame, schriftliche Vereinbarung darüber, was gefragt werden kann, und einen Weg, sie zu ändern, ohne die zu verärgern, die sich darauf verlassen.
In der oberen Reihe ruft die Kunden-App den Bestellservice auf, der den Zahlungsservice aufruft, und jeder Schritt wartet. Während er wartet, ist der Bestellservice blockiert. Alle drei müssen gleichzeitig laufen, also addieren sich Latenzen und Verfügbarkeiten multiplizieren sich. Das ist zeitliche Kopplung. In der unteren Reihe sendet der Bestellservice eine Nachricht „Bestellung aufgegeben“ an eine Queue und macht weiter. Die Nachricht wartet, bis der Benachrichtigungsservice bereit ist, sodass der Empfänger beschäftigt oder kurz ausgefallen sein kann, ohne dass der Sender es merkt. Aber Asynchronität verlagert Komplexität, statt sie zu beseitigen. Beide Seiten hängen weiterhin von Format und Bedeutung der Nachricht ab. Eine gute Regel: Rufen Sie nur dann synchron auf, wenn der Aufrufer ohne die Antwort wirklich nicht weitermachen kann.
REST, kurz für Representational State Transfer, nutzt Ressourcen, Methoden und Statuscodes so, wie HTTP sie definiert. Es lässt sich mit Standardwerkzeugen gut cachen und eignet sich für öffentliche und Partner-APIs. gRPC sendet kompakte Binärnachrichten mit typisiertem Code, der aus einer Vertragsdatei generiert wird. Es ist für interne Aufrufe zwischen Services beliebt, aber Browser können es nicht direkt aufrufen, und seine Nachrichten sind in Logs schwer zu lesen. Mit GraphQL fragt jeder Client in einem Roundtrip genau die Felder ab, die er braucht. Aber das Serverteam muss teure Abfragen begrenzen und Berechtigungen Feld für Feld prüfen. Und Messaging über Queues und Topics eignet sich für Arbeit, die später erledigt werden kann, für Fan-out an viele Consumer und zum Glätten der Last.
Nun zu Wiederholungen. In HTTP liest GET nur, daher ist es gefahrlos wiederholbar. PUT und DELETE sind idempotent. Aber POST ist keines von beiden, daher kann eine Wiederholung eine zweite Bestellung anlegen und den Kunden doppelt belasten. Die Lösung ist ein Idempotenzschlüssel. Die App sendet ihre Bestellung mit einem eindeutigen Schlüssel. Die API speichert Bestellung und Schlüssel zusammen, und die Datenbank bestätigt Bestellung zehn zweiundvierzig. Doch die Antwort geht auf dem Rückweg verloren, also läuft die App in ein Timeout und versucht es erneut. Die Wiederholung trägt denselben Schlüssel. Die API stellt fest, dass er schon erledigt ist, und liefert die gespeicherte Antwort für Bestellung zehn zweiundvierzig. Keine zweite Bestellung, keine zweite Belastung.
Das ausgearbeitete Beispiel des Abschnitts fügt drei Regeln hinzu. Tippt der Kunde erneut, während die erste Anfrage noch läuft, antwortet der Service mit einem Konflikt, und die App wartet und versucht es erneut. Zwei Kopien laufen nie gleichzeitig. Sendet ein Bug denselben Schlüssel mit einem anderen Warenkorb, lehnt der Service ihn ab, statt stillschweigend die falsche Bestellung zu liefern. Und Schlüssel verfallen nach einer veröffentlichten Frist, hier vierundzwanzig Stunden. Der Schlüssel wird einmal pro logischer Operation erzeugt, nicht pro Versuch, und in derselben Transaktion wie die Bestellung gespeichert, sodass ein Absturz sie nicht trennen kann.
Eine API mit Consumern ist ein Versprechen. Abwärtskompatible Änderungen, etwa ein optionales Feld oder ein neuer Endpoint, sind jederzeit möglich. Ein Feld zu entfernen oder umzubenennen bricht Consumer. Die Bedeutung eines Felds zu ändern ist der gefährlichste Bruch, weil nichts sichtbar scheitert. Consumer helfen als Tolerant Reader: Sie ignorieren Felder, die sie nicht verstehen, sodass Ergänzungen sie nie brechen. Ist ein Bruch unvermeidbar, versionieren Sie die API. Zwischen Ihren eigenen Services können Sie stattdessen Expand and Contract nutzen: das neue Feld hinzufügen, jeden Consumer umstellen, dann das alte entfernen. Und mit Consumer-Driven-Contract-Tests legt jeder Consumer fest, was er tatsächlich nutzt, sodass der Build des Providers zeigt, welche Änderungen wen brechen würden. Das macht unabhängiges Deployment sicher.
Der vollständige Abschnitt behandelt REST in seiner ursprünglichen Definition, Paginierung, Fehlerformate und Vertragswerkzeuge. Als Nächstes, in Abschnitt sechs Punkt sechs, betrachten wir Konsistenzmodelle, CAP und PACELC. Bis gleich.
Was Sie wissen müssen
- Synchrone Aufrufe erzeugen zeitliche Kopplung: Aufrufer und Aufgerufener müssen gleichzeitig laufen, sodass sich Latenzen entlang einer Kette addieren und Verfügbarkeiten multiplizieren; asynchrones Messaging beseitigt diese Kopplung, nicht aber die Format- oder semantische Kopplung.
- REST ist Fieldings Satz von Einschränkungen (Client-Server, zustandslos, cachebar, einheitliche Schnittstelle, Schichtensystem, optional Code on Demand); die meisten realen APIs erreichen Stufe 2 von Richardsons Reifegradmodell, nicht Stufe 3 mit Hypermedia.
- RFC 9110 definiert GET, HEAD, OPTIONS, PUT und DELETE als idempotent und POST als weder sicher noch idempotent; Clients sollten nach einem Timeout, 502, 503 oder 504 nur idempotente Anfragen automatisch wiederholen, daher braucht POST einen Idempotenzschlüssel, bevor es sicher wiederholt werden kann.
- gRPC nutzt Protocol Buffers über HTTP/2 mit generiertem Code und Streaming und eignet sich für interne Aufrufe; GraphQL lässt unterschiedliche Clients Felder wählen, braucht aber Batching, Grenzen für Abfragekosten und Autorisierung auf Feldebene.
- Queues stellen jede Nachricht einem von mehreren konkurrierenden Consumern zu; Topics stellen jedem Abonnenten eine Kopie zu.
- Langlebige APIs modellieren fachliche Ressourcen, paginieren mit Cursorn, liefern Problem Details nach RFC 9457, ändern sich nur abwärtskompatibel und versionieren oder nutzen Expand and Contract, wenn sie brechen müssen.
- OpenAPI und AsyncAPI beschreiben HTTP- und Nachrichtenverträge; Consumer-Driven-Contract-Tests wie Pact zeigen Providern, welche Änderungen welche Consumer brechen würden.
Zwei Arten der Kommunikation und die Kopplung, die jede erzeugt
Sobald ein System in getrennt deployte Teile zerlegt ist (siehe Warum verteilen), müssen diese Teile über ein Netzwerk kommunizieren, und die Art, wie sie kommunizieren, prägt das System ebenso stark wie die Art, wie es aufgeteilt ist. Es gibt zwei grundlegende Modi. Bei synchroner Kommunikation sendet der Aufrufer eine Anfrage und wartet auf die Antwort, bevor er weitermacht, wie bei einem Telefonanruf. Bei asynchroner Kommunikation übergibt der Sender eine Nachricht an einen Vermittler, meist einen Message Broker, und macht weiter; der Empfänger holt die Nachricht ab, wenn er bereit ist, wie einen Brief aus dem Briefkasten. Abschnitt 0.6 hat den Unterschied eingeführt; hier wird er als Architekturentscheidung untersucht.
Synchrone Aufrufe erzeugen zeitliche Kopplung: Beide Seiten müssen im selben Moment laufen und erreichbar sein. Der Aufrufer ist daher bestenfalls so verfügbar wie das Produkt der Verfügbarkeiten seiner Abhängigkeiten, und seine Antwortzeit, einschließlich seiner Tail-Latenz, enthält deren Antwortzeiten: die Arithmetik aus Abschnitt 6.1. Ein langsamer Zahlungsservice macht den Bestellservice langsam, und wenn alle Threads des Bestellservice auf ihn warten, antwortet der Bestellservice niemandem mehr. Timeouts, Wiederholungen und Circuit Breaker begrenzen den Schaden; sie werden unter Resilienzmuster behandelt.
Asynchrones Messaging beseitigt die zeitliche Kopplung: Der Broker speichert die Nachricht, sodass der Empfänger langsam, beschäftigt oder kurz ausgefallen sein kann, ohne dass der Sender es bemerkt. Es beseitigt nicht jede Art von Kopplung. Beide Seiten hängen weiterhin vom Format und von der Bedeutung der Nachricht ab (Format- und semantische Kopplung) sowie vom Broker selbst, der zu kritischer Infrastruktur wird. Außerdem erfährt der Sender das Ergebnis nicht mehr sofort, daher muss der Entwurf mit Arbeit umgehen, die später scheitert, mit Nachrichten, die doppelt ankommen, und mit Nachrichten, die in anderer Reihenfolge ankommen. Asynchronität verlagert Komplexität, statt sie zu beseitigen.
Eine nützliche Faustregel: Nutzen Sie einen synchronen Aufruf, wenn der Aufrufer ohne die Antwort wirklich nicht weitermachen kann („Ist diese Karte autorisiert?“), und asynchrones Messaging, wenn der Aufrufer nur wissen muss, dass die Arbeit erledigt werden wird („Beleg senden“, „Restaurant informieren“). Viele gute Entwürfe halten den Pfad, den Nutzer sehen, kurz und synchron und schieben alles andere hinter eine Queue.
Zeitliche Kopplung mit und ohne Broker
REST, wie Fielding es definiert hat
REST (Representational State Transfer) wurde von Roy Fielding in seiner Dissertation von 2000 als Architekturstil für große, vernetzte Hypermedia-Systeme definiert: das Web selbst. Fielding leitete ihn als Satz von Einschränkungen her, von denen jede ein Qualitätsmerkmal einbringt. Client-Server trennt die Benutzeroberfläche von der Datenspeicherung, sodass sich beide unabhängig weiterentwickeln. Zustandslose Interaktion bedeutet, dass jede Anfrage alles enthält, was zu ihrem Verständnis nötig ist, sodass jede Serverinstanz sie bearbeiten kann, was Skalierbarkeit und Wiederherstellung hilft. Cachebare Antworten geben an, ob sie wiederverwendet werden dürfen, was Latenz und Last senkt. Eine einheitliche Schnittstelle (Ressourcen, die durch URIs identifiziert und über Repräsentationen manipuliert werden, selbstbeschreibende Nachrichten und Hypermedia als Motor des Anwendungszustands) lässt generische Vermittler wie Caches und Proxys den Datenverkehr verstehen. Ein Schichtensystem lässt Proxys, Gateways und Load Balancer unsichtbar im Pfad sitzen. Code on Demand, etwa an einen Browser gesendetes JavaScript, ist die einzige optionale Einschränkung.
Die meisten als RESTful bezeichneten APIs erfüllen nur einige davon. Leonard Richardsons Reifegradmodell, 2008 vorgestellt und 2010 von Martin Fowler bekannt gemacht, beschreibt die Stufen. Auf Stufe 0 ist HTTP nur ein Tunnel: Jeder Aufruf ist ein POST an einen einzigen Endpoint, die Operation steht im Body, wie bei SOAP. Auf Stufe 1 hat die API getrennte Ressourcen (/orders/1042, /restaurants/7). Auf Stufe 2 nutzt sie HTTP-Methoden und Statuscodes so, wie HTTP sie definiert. Auf Stufe 3 enthalten Antworten Hypermedia Controls: Links, die dem Client sagen, was er als Nächstes tun kann (eine aufgegebene Bestellung enthält einen Link zum Stornieren, bis eine Stornierung nicht mehr möglich ist).
Über Stufe 3 sind sich Fachleute ernsthaft uneinig. Fielding besteht darauf, dass eine API ohne Hypermedia nicht REST ist. Die meisten öffentlichen APIs bleiben auf Stufe 2, weil ihre Clients anhand der Dokumentation geschrieben werden, statt zur Laufzeit entdeckt zu werden, und der Nutzen des Link-Folgens den zusätzlichen Entwurfsaufwand selten aufwiegt. Eine pragmatische Lesart ist, dass Stufe 2, konsequent umgesetzt, den größten Teil des Werts bringt; Hypermedia lohnt sich, wenn der Server viele unabhängig geschriebene Clients durch einen sich ändernden Ablauf steuern muss.
HTTP-Semantik, die für die Architektur zählt
Die Semantik von HTTP ist in RFC 9110 (2022) definiert. Zwei Eigenschaften von Methoden zählen für verteilte Entwürfe mehr als alle anderen. Eine sichere Methode liest nur: GET, HEAD und OPTIONS ändern nichts, was der Client ändern wollte. Eine idempotente Methode darf den Zustand ändern, aber sie zu wiederholen hat dieselbe beabsichtigte Wirkung wie ein einmaliges Senden: PUT (diese Ressource durch diese Repräsentation ersetzen) und DELETE sind idempotent, ebenso alle sicheren Methoden. POST ist keines von beiden, und PATCH ist im Allgemeinen nicht idempotent. Die Eigenschaft zählt, weil Netzwerke zwischen der Verarbeitung einer Anfrage und dem Eintreffen der Antwort ausfallen. RFC 9110 sagt, dass idempotente Anfragen nach einem solchen Ausfall automatisch wiederholt werden können und dass Clients nicht idempotente Anfragen nicht automatisch wiederholen sollten, es sei denn, sie wissen, dass die Anfrage tatsächlich gefahrlos wiederholt werden kann.
Statuscodes sind Teil des Vertrags, weil Client-Code anhand von ihnen verzweigt; Abschnitt 0.6 hat die gängigen eingeführt. Erfolgscodes sollten sagen, was passiert ist: 201 Created (mit einem Location-Header), wenn eine Ressource angelegt wird, 202 Accepted, wenn die Arbeit nur in eine Queue gestellt wurde, 204 No Content für einen Erfolg ohne Body. Bei Konflikten bedeutet 409, dass die Anfrage mit dem aktuellen Zustand der Ressource kollidiert, 412, dass eine Vorbedingung nicht erfüllt war, und 422, dass eine wohlgeformte Anfrage semantisch ungültig ist. Die architektonische Regel betrifft Wiederholungen: Eine 4xx-Antwort wird erneut scheitern, wenn die Anfrage unverändert gesendet wird (429 Too Many Requests ist die Ausnahme: so lange warten wie angegeben, dann wiederholen), während ein Timeout, 502, 503 oder 504 später gelingen kann. Clients sollten nur diese wiederholen, mit Backoff und Jitter, und nur, wenn die Anfrage idempotent ist oder einen Idempotenzschlüssel trägt.
Zwei weitere HTTP-Features lösen verteilte Probleme günstig. Bedingte Anfragen ermöglichen optimistische Nebenläufigkeitskontrolle: Ein Client liest eine Bestellung mit ihrem Versionskennzeichen (ETag) und sendet beim Aktualisieren If-Match mit diesem Kennzeichen; hat jemand anderes die Bestellung inzwischen geändert, antwortet der Server mit 412, und nichts wird überschrieben. Caching-Header (Cache-Control, ETag) lassen Browser, CDNs und Proxys Antworten wiederverwenden, was oft die günstigste verfügbare Performance-Taktik ist.
HTTP-Methoden: sicher, idempotent oder keines von beiden (RFC 9110; PATCH aus RFC 5789)
| Methode | Typische Verwendung | Sicher? | Idempotent? | Blind wiederholbar? |
|---|---|---|---|---|
| GET, HEAD | Eine Ressource lesen | Ja | Ja | Ja |
| PUT | An einer bekannten URI anlegen oder ersetzen | Nein | Ja | Ja |
| DELETE | Eine Ressource entfernen | Nein | Ja | Ja (ein zweiter Aufruf kann 404 liefern, aber die Wirkung ist dieselbe) |
| POST | In einer Collection anlegen oder eine Aktion ausführen | Nein | Nein | Nur mit einem Idempotenzschlüssel oder einer anderen Duplikatprüfung |
| PATCH | Teilaktualisierung | Nein | Im Allgemeinen nicht | Nur wenn der Patch idempotent geschrieben ist oder an eine Version gebunden ist |
gRPC: zuerst der Vertrag, binär auf der Leitung
gRPC ist ein Framework für Remote Procedure Calls, das Google 2015 als Nachfolger von Stubby als Open Source veröffentlichte, dem RPC-System, das es über ein Jahrzehnt intern genutzt hatte; Version 1.0 folgte 2016. Der Vertrag wird in einer .proto-Datei mit Protocol Buffers geschrieben, einem kompakten binären Serialisierungsformat, und Codegeneratoren erzeugen typisierte Client-Stubs und Server-Skelette in vielen Sprachen. Aufrufe laufen über HTTP/2, das viele gleichzeitige Anfragen über eine Verbindung multiplext.
gRPC bietet vier Arten von Aufrufen: unär (eine Anfrage, eine Antwort), Server-Streaming (eine Anfrage, ein Strom von Antworten, etwa Live-Positionen der Fahrer), Client-Streaming (ein Strom von Anfragen, eine Antwort, etwa das Hochladen von Telemetrie) und bidirektionales Streaming. Clients setzen eine Deadline; ist sie abgelaufen, endet der Aufruf mit DEADLINE_EXCEEDED, und Deadlines lassen sich entlang einer Aufrufkette weitergeben, sodass die Arbeit überall stoppt, sobald niemand mehr auf die Antwort wartet.
Seine Stärken sind Effizienz, starke Typisierung und generierter Code, was es für interne Aufrufe zwischen Services in polyglotten Organisationen beliebt macht. Seine Kosten sind das Spiegelbild davon. Binäre Payloads sind in Logs und mit gewöhnlichen Werkzeugen schwer zu lesen. Browser können gRPC nicht direkt aufrufen, daher brauchen öffentliche und webseitige APIs meist einen Proxy oder eine REST- oder GraphQL-Fassade. HTTP-Caching greift nicht auf die übliche Weise. Kompatibilität hängt von Disziplin bei den Feldnummern ab: Die Nummer eines Felds, nicht sein Name, identifiziert es auf der Leitung, daher dürfen Nummern nie wiederverwendet werden, und entfernte Felder sollten als reserved markiert werden.
syntax = "proto3";
service Dispatch {
rpc AssignCourier (AssignRequest) returns (Assignment);
rpc TrackCourier (TrackRequest) returns (stream Position);
}
message AssignRequest {
string order_id = 1;
string restaurant_id = 2;
reserved 3; // field removed in v2: never reuse 3
}
message Position {
double lat = 1;
double lng = 2;
int64 recorded_at_ms = 3;
}GraphQL: Der Client bestimmt die Form
GraphQL ist eine Abfragesprache und Laufzeitumgebung für APIs, die Facebook ab 2012 intern für seine mobilen Apps entwickelte und 2015 mit einem Spezifikationsentwurf und einer Referenzimplementierung öffentlich freigab; 2018 ging sie an die herstellerneutrale GraphQL Foundation. Der Server veröffentlicht ein typisiertes Schema; Clients senden Abfragen an einen einzigen Endpoint, die genau die gewünschten Felder nennen, möglicherweise über verknüpfte Objekte hinweg, und erhalten eine JSON-Antwort derselben Form. Mutations ändern Daten, und Subscriptions schicken Aktualisierungen aktiv an den Client.
Ihr Hauptnutzen liegt bei unterschiedlichen Clients. Eine mobile App der Essenslieferplattform mit langsamer Verbindung kann in einem Roundtrip den Status einer Bestellung, den Namen des Restaurants und den Vornamen des Kuriers abfragen, ohne die zwanzig anderen Felder zu erhalten, die das Web-Dashboard braucht. Das vermeidet sowohl Over-Fetching als auch die Kette von Anfragen, die eine REST-API Ressource für Ressource erfordern kann, und kann ein eigenes Backend für jedes Frontend ersetzen. Das Schema dokumentiert sich außerdem selbst und lässt Werkzeuge Abfragen prüfen, bevor sie laufen.
Die Abwägungen sind real und werden größtenteils vom Serverteam getragen. Jedes Feld wird von einem Resolver befüllt, daher erzeugt eine naive Implementierung, die das Restaurant jeder Bestellung einzeln abruft, das N+1-Abfrageproblem: eine Abfrage für fünfzig Bestellungen, dann fünfzig weitere für ihre Restaurants. Die übliche Abhilfe sind Batching und Caching pro Anfrage, wie es die Bibliothek DataLoader bekannt gemacht hat. Weil Clients beliebig tiefe Abfragen zusammensetzen können, brauchen Server Grenzen für Abfragetiefe und -kosten, Timeouts und oft eine Allow-List genehmigter (persistierter) Abfragen, um versehentliche oder böswillige Überlastung zu verhindern. HTTP-Caching ist schwächer, wenn alles ein POST an eine URL ist. Autorisierung muss pro Feld oder pro Objekt durchgesetzt werden statt pro URL. Und Fehler werden typischerweise in einem errors-Array innerhalb einer erfolgreichen HTTP-Antwort gemeldet, was ein Monitoring auf Basis von Statuscodes übersieht.
Messaging: Queues und Topics
Messaging verlagert die Kommunikation in einen Broker. Wie ereignisgesteuerte Architektur erklärt, stellt eine Queue jede Nachricht einem von mehreren konkurrierenden Consumern zu, der Standardweg, einen Worker-Pool zu skalieren, während ein Topic jedem Abonnenten eine Kopie zustellt, sodass die Services für Restaurant, Disposition und Benachrichtigungen jeweils auf OrderPlaced reagieren können, ohne dass der Bestellservice von ihnen weiß. Dieser Abschnitt behandelt auch Zustellgarantien, Reihenfolge, Duplikate und Dead Letter Queues; Queues und Backpressure behandelt, wie Queues Lastspitzen abfangen.
Messaging kann auch Anfrage und Antwort transportieren: Der Anfragende sendet ein Command mit einer Korrelations-ID und einer Antwortadresse und ordnet die spätere Antwort seiner Anfrage zu. So bleibt die zeitliche Entkopplung eines Brokers erhalten, und man bekommt trotzdem eine Antwort, um den Preis von mehr beweglichen Teilen.
REST, gRPC, GraphQL und Messaging im Vergleich
| Aspekt | REST über HTTP | gRPC | GraphQL | Messaging (Queues und Topics) |
|---|---|---|---|---|
| Interaktion | Synchron, Anfrage-Antwort | Synchron; unär oder Streaming | Synchrone Queries und Mutations; Subscriptions pushen | Asynchron; Fire-and-Forget oder Anfrage-Antwort über Korrelations-IDs |
| Vertrag | OpenAPI-Dokument (optional, aber üblich) | .proto-Dateien (erforderlich) | Schema (erforderlich) | AsyncAPI-Dokument und Nachrichtenschemas (optional, aber ratsam) |
| Payload | Meist JSON-Text | Protocol Buffers, binär | JSON | Beliebig: JSON, Avro, Protocol Buffers |
| Zeitliche Kopplung | Ja | Ja | Ja | Nein: Der Broker puffert |
| Caching | Ausgezeichnet: Standard-HTTP-Caches und CDNs | Wenig eingebaut | Schwieriger: braucht persistierte Abfragen oder Client-Caches | Nicht anwendbar |
| Browser-Unterstützung | Nativ | Braucht einen Proxy (gRPC-Web) | Nativ | Nicht direkt; über ein Backend oder WebSockets |
| Am besten geeignet für | Öffentliche und Partner-APIs; ressourcenorientiertes CRUD | Interne Service-Aufrufe mit hohem Volumen und niedriger Latenz; Streaming | Viele Client-Typen mit unterschiedlichem Datenbedarf; Bündeln mehrerer Services | Arbeit, die später erledigt werden kann; Fan-out an viele Consumer; Glätten der Last |
| Hauptfallen | Gesprächige (chatty) APIs; uneinheitliche Konventionen | Undurchsichtig beim Debugging; Disziplin bei Feldnummern | N+1-Resolver; unbegrenzte Abfragekosten; Autorisierung auf Feldebene | Duplikate, Reihenfolge, unsichtbare Abläufe, Betrieb des Brokers |
APIs entwerfen, die Bestand haben: Ressourcen, Paginierung und Filterung
Modellieren Sie Ressourcen nach der Fachdomäne, nicht nach der Datenbank. Legen Sie die Substantive offen, in denen Ihre Consumer denken (/orders, /orders/1042/items, /restaurants/7/menu), nicht Tabellen oder interne Services. Aktionen, die nicht zu Anlegen, Lesen, Aktualisieren und Löschen passen, lassen sich meist selbst als Ressourcen modellieren: POST /orders/1042/cancellation legt eine Stornierung an, die einen Grund tragen und mit 409 abgelehnt werden kann, wenn das Essen schon unterwegs ist. So bleibt die einheitliche Schnittstelle erhalten, und der Geschäftsprozess wird trotzdem ehrlich ausgedrückt.
Paginieren Sie jede Collection, denn Collections wachsen. Offset-Paginierung (?offset=40&limit=20) ist einfach und erlaubt den Sprung auf Seite 9, wird aber auf tiefen Seiten langsam, weil die Datenbank alle früheren Zeilen überspringen muss, und sie zeigt Duplikate oder lässt Einträge aus, wenn Zeilen eingefügt werden, während jemand blättert. Cursor-Paginierung liefert ein undurchsichtiges Token, das direkt hinter den letzten Eintrag zeigt (?after=eyJpZCI6MTA0Mn0&limit=20); sie bleibt bei Einfügungen schnell und stabil, um den Preis, dass kein wahlfreier Zugriff möglich ist. Für Feeds und große oder sich ändernde Daten sind Cursor der sicherere Standard. Begrenzen Sie die Seitengröße immer auf dem Server.
Filtern und Sortieren sollten vorhersehbare Query-Parameter nutzen (?status=delivered&placed_after=2026-10-01&sort=-placed_at) und durch Indizes gestützt sein; eine API, die Clients nach jedem beliebigen Feld filtern lässt, lädt zu Abfragen ein, die die Datenbank nicht schnell beantworten kann. Unterstützen Sie nur die Filter, die Consumer brauchen, und dokumentieren Sie sie im Vertrag.
Idempotenzschlüssel: POST sicher wiederholbar machen
Ein Teilausfall (Abschnitt 6.1) lässt einen Client, der keine Antwort bekommen hat, im Unklaren darüber, ob seine Anfrage verarbeitet wurde. Ein GET zu wiederholen ist harmlos; POST /orders zu wiederholen kann eine zweite Bestellung anlegen und den Kunden doppelt belasten. Ein Idempotenzschlüssel macht aus der unsicheren Wiederholung eine sichere. Der Client erzeugt für jede logische Operation einen eindeutigen Wert (typischerweise eine zufällige UUID) und sendet ihn in einem Header wie Idempotency-Key. Der Server speichert den Schlüssel zusammen mit dem Ergebnis der ersten Anfrage; jede spätere Anfrage mit demselben Schlüssel liefert das gespeicherte Ergebnis, statt die Arbeit erneut auszuführen.
Zahlungsanbieter haben die Technik bekannt gemacht, und die IETF-Arbeitsgruppe HTTP APIs hat einen Standard-Header Idempotency-Key entworfen. Zum Zeitpunkt der Erstellung ist er nicht als RFC veröffentlicht (der jüngste Entwurf vom Oktober 2025 ist abgelaufen), aber seine Regeln spiegeln die gängige Praxis: Ein Schlüssel, der mit einem anderen Request-Body wiederverwendet wird, sollte mit 422 abgelehnt werden, eine Wiederholung, die eintrifft, während die erste Anfrage noch läuft, sollte 409 Conflict erhalten, damit nie zwei Kopien gleichzeitig laufen, und Schlüssel gelten pro Client und verfallen nach einer veröffentlichten Frist, oft 24 Stunden. Das ausgearbeitete Beispiel wendet jede Regel an.
Eine wiederholte Bestellung mit Idempotenzschlüssel
Ausgearbeitetes Beispiel
Ausgearbeitetes Beispiel: der Doppeltipp beim Bezahlvorgang
Ein Kunde mit schwacher Mobilfunkverbindung tippt auf „Bestellen“. Die App erzeugt den Schlüssel 7f3c… und sendet POST /v1/orders. Der Bestellservice speichert den Schlüssel zusammen mit einem Hash des Request-Bodys und später der Antwort.
| Versuch 1 | Bestellung 1042 wird angelegt und die Zahlung autorisiert; Schlüssel, Body-Hash und Antwort 201 werden in derselben Datenbanktransaktion wie die Bestellung gespeichert. Die Antwort geht verloren, als das Smartphone von WLAN auf mobile Daten wechselt. |
|---|---|
| Versuch 2 (automatische Wiederholung, derselbe Schlüssel) | Der Service findet den Schlüssel mit dem Status abgeschlossen, prüft, ob der Body-Hash übereinstimmt, und liefert die gespeicherte 201 für Bestellung 1042. Keine zweite Bestellung, keine zweite Belastung. |
| Versuch 3 (der Kunde tippt erneut, während Versuch 2 noch unterwegs ist) | Die App verwendet für denselben Warenkorb denselben Schlüssel erneut. Würde die erste Anfrage noch verarbeitet, antwortete der Service mit 409, und die App würde warten und es erneut versuchen. |
| Ein Bug sendet denselben Schlüssel mit einem anderen Warenkorb | Die Body-Hashes unterscheiden sich: Der Service antwortet mit 422, statt stillschweigend die falsche Bestellung zu liefern. |
| Nach 24 Stunden | Der Schlüssel verfällt und wird gelöscht; die App wiederholt diesen alten Warenkorb nie. |
Der Schlüssel muss einmal pro logischer Operation erzeugt werden (pro Absenden eines Warenkorbs), nicht pro HTTP-Versuch; ein neuer Schlüssel bei jeder Wiederholung verfehlt den Zweck.
Den Schlüssel in derselben Transaktion wie die Bestellung zu speichern, macht die Prüfung zuverlässig: Ein Absturz kann keine Bestellung ohne ihren Schlüssel und keinen Schlüssel ohne seine Bestellung hinterlassen.
Dieselbe Idee, auf Nachrichten statt auf HTTP-Anfragen angewandt, ist der idempotente Consumer der ereignisgesteuerten Architektur.
def create_order(request, db):
key = request.headers.get("Idempotency-Key")
if not key:
return problem(400, "missing-idempotency-key")
body_hash = sha256(request.body)
with db.transaction():
row = db.find_idempotency(request.client_id, key)
if row and row.body_hash != body_hash:
return problem(422, "idempotency-key-reused")
if row and row.status == "in_progress":
return problem(409, "request-in-progress")
if row:
return row.saved_response # replay, do nothing new
db.insert_idempotency(request.client_id, key, body_hash, "in_progress")
order = db.insert_order(request.json)
response = created(order)
db.complete_idempotency(request.client_id, key, response)
return responseFehler, die Maschinen lesen können: RFC 9457
Clients brauchen Fehler, auf die sie programmatisch reagieren können, und Menschen brauchen Fehler, die sie verstehen. Statt dass jedes Team ein eigenes Fehlerformat erfindet, nutzen Sie Problem Details for HTTP APIs, RFC 9457 (Juli 2023), das RFC 7807 (2016) abgelöst hat. Ein Problem-Details-Objekt hat den Medientyp application/problem+json und fünf Standardfelder: type (eine URI, die die Art des Problems identifiziert und auf Dokumentation verweisen kann), title (eine kurze, menschenlesbare Zusammenfassung dieser Art), status (der HTTP-Statuscode), detail (eine Erklärung dieses Vorkommens) und instance (eine URI, die dieses Vorkommen identifiziert). Erweiterungsfelder können maschinenlesbare Einzelheiten tragen, etwa welche Felder die Validierung nicht bestanden haben.
Zwei Regeln halten Fehlerantworten sicher. Client-Code sollte anhand von type und status verzweigen, nie anhand des Wortlauts von title oder detail, der übersetzt oder verbessert werden kann. Und Fehler dürfen keine Interna preisgeben: Stacktraces, SQL-Fragmente und interne Hostnamen helfen Angreifern mehr als Aufrufern. Protokollieren Sie die Einzelheiten mit einer Korrelations-ID und liefern Sie stattdessen die ID zurück.
{
"type": "https://api.fooddelivery.example/problems/outside-delivery-area",
"title": "Address is outside the delivery area",
"status": 422,
"detail": "Restaurant 7 delivers within 4 km; the address is 6.2 km away.",
"instance": "/v1/orders/attempts/9b1e",
"max_distance_km": 4,
"actual_distance_km": 6.2
}Eine API weiterentwickeln, ohne ihre Consumer zu brechen
Eine API mit Consumern ist ein Versprechen, und Hyrums Gesetz (Abschnitt 0.6) warnt, dass jemand von jedem beobachtbaren Verhalten abhängt. Änderungen folgen daher einem Prinzip: Nehmen Sie abwärtskompatible Änderungen frei vor und vermeiden Sie brechende. Consumer helfen, indem sie Tolerant Reader sind (ein Begriff, den Martin Fowler verwendet): Sie ignorieren Felder, die sie nicht verstehen, und lesen nur, was sie brauchen, sodass Ergänzungen sie nie brechen. Die Tabelle unten fasst die üblichen Regeln zusammen; dieselbe Logik, angewandt auf gespeicherte Daten und Event-Schemas, findet sich unter Schemaevolution.
Ist eine brechende Änderung unvermeidbar, versionieren Sie. Die gängigen Strategien sind eine Version im URL-Pfad (/v2/orders: explizit, leicht zu routen und zu cachen, verleitet aber zu großen, seltenen Sprüngen), ein eigener Header oder ein Parameter im Medientyp (Accept: application/vnd.fooddelivery.v2+json: hält URLs stabil, ist aber weniger sichtbar und schwerer im Browser zu testen) und datierte Versionen, die pro Client-Konto festgelegt werden und die einige große API-Anbieter nutzen, um kleine brechende Änderungen fortlaufend zu veröffentlichen, während jeder Client aktualisiert, wann er will. Interne APIs zwischen Services derselben Organisation vermeiden Versionen oft ganz durch den Ansatz Expand and Contract: das neue Feld oder den neuen Endpoint hinzufügen, jeden Consumer migrieren, dann das alte entfernen. Unabhängig von der Strategie: Kündigen Sie Abkündigungen im Voraus an; die Response-Header Deprecation (RFC 9745, 2025) und Sunset (RFC 8594, 2019) lassen einen Server Clients in maschinenlesbarer Form mitteilen, dass ein Endpoint abgekündigt ist und wann er nicht mehr funktionieren wird.
Abwärtskompatible und brechende Änderungen an einer API
| Änderung | Kompatibel? | Warum |
|---|---|---|
| Ein optionales Anfragefeld oder einen Query-Parameter hinzufügen | Ja | Bestehende Clients senden es nicht; der Server muss einen sinnvollen Standardwert anwenden. |
| Ein Feld zu einer Antwort hinzufügen | Meistens | Tolerant Reader ignorieren es; strikte Clients, die unbekannte Felder ablehnen, brechen, daher sollten Sie dokumentieren, dass Clients Ergänzungen tolerieren müssen. |
| Einen neuen Endpoint oder eine neue Ressource hinzufügen | Ja | Noch ruft ihn niemand auf. |
| Einer Aufzählung in einer Antwort einen Wert hinzufügen | Riskant | Clients mit erschöpfenden switch-Anweisungen können am unbekannten Wert scheitern; dokumentieren Sie, dass neue Werte auftreten können. |
| Ein optionales Anfragefeld zur Pflicht machen | Nein | Bestehende Clients, die es weglassen, scheitern ab sofort mit 400. |
| Ein Feld entfernen oder umbenennen oder seinen Typ oder seine Einheit ändern | Nein | Clients, die das alte Feld lesen, bekommen nichts oder das Falsche. |
| Die Bedeutung eines Felds oder Statuscodes ändern | Nein | Der gefährlichste Bruch: Nichts scheitert sichtbar, aber das Verhalten ist falsch. |
| Die Validierung verschärfen (kürzere Maximallänge, strengeres Format) | Nein | Anfragen, die früher gelangen, scheitern jetzt. |
Verträge beschreiben: OpenAPI und AsyncAPI
Ein in einem maschinenlesbaren Format geschriebener Vertrag kann Dokumentation, Client-Generierung, Server-Stubs, Mock-Server, Linting und Tests antreiben. OpenAPI ist der Standard für HTTP-APIs. Es begann als Swagger-Spezifikation, die SmartBear 2015 der neu gegründeten OpenAPI Initiative unter dem Dach der Linux Foundation übergab und die 2016 umbenannt wurde; Version 3.1 (2021) glich ihre Schemas an JSON Schema an, und Version 3.2 folgte im September 2025. AsyncAPI wendet dieselbe Idee auf nachrichtengesteuerte APIs an: Es beschreibt Kanäle (Queues und Topics), die Operationen, die darauf senden oder empfangen, und die Nachrichtenschemas; Version 3.0 erschien im Dezember 2023.
Teams wählen zwischen Design-First (den Vertrag schreiben und prüfen, dann implementieren) und Code-First (den Vertrag aus annotiertem Code generieren). Design-First macht die API zu einer bewussten Entscheidung, die Consumer prüfen können, bevor etwas gebaut wird, und eignet sich für öffentliche und teamübergreifende APIs; Code-First hält Vertrag und Implementierung mit weniger Aufwand im Gleichschritt und eignet sich für interne APIs, die sich schnell ändern. In beiden Fällen gilt: Halten Sie den Vertrag in der Versionsverwaltung, linten Sie ihn in der Pipeline und führen Sie eine Prüfung auf Breaking Changes aus, die jede vorgeschlagene Version mit der veröffentlichten vergleicht.
Contract Testing und Consumer-Driven Contracts
End-to-End-Tests, die alle Services zusammen deployen, sind langsam, unzuverlässig und werden mit jedem weiteren Service langsamer, doch Unit-Tests können nicht erkennen, dass ein Provider ein Feld geändert hat, auf das sich ein Consumer verlässt. Contract Testing schließt die Lücke: Jede Seite wird isoliert gegen eine gemeinsame Beschreibung ihrer Interaktionen getestet. Bei Consumer-Driven Contracts, einem Muster, das Ian Robinson 2006 beschrieb, legt jeder Consumer die Teile der Provider-API fest, die er tatsächlich nutzt, und der Provider führt die Erwartungen aller seiner Consumer als Teil seines eigenen Builds aus. Der Provider weiß dann genau, welche Felder niemand liest (gefahrlos änderbar) und welche Änderungen einen namentlich bekannten Consumer brechen würden.
Pact, 2013 bei realestate.com.au entstanden, ist das bekannteste Werkzeug. Die Tests des Consumers laufen gegen einen Pact-Mock-Provider und zeichnen die gestellten Anfragen und die erwarteten Antworten in einer Vertragsdatei auf; die Pipeline des Providers spielt diese Anfragen gegen den echten Provider ab und prüft die Antworten. Ein Broker speichert Verträge und Verifikationsergebnisse, sodass eine Pipeline vor dem Deployment fragen kann, ob diese Version eines Service mit den Versionen seiner Consumer und Provider kompatibel ist, die bereits in Produktion laufen. Contract-Tests ersetzen nicht einige wenige End-to-End-Smoke-Tests, und sie prüfen die Struktur statt der fachlichen Bedeutung, aber sie erlauben es, Services mit Zuversicht unabhängig zu deployen, und genau darum geht es beim Aufteilen.
Prüfen Sie Ihr Verständnis
Warum beseitigt eine Queue zwischen zwei Services nicht jede Kopplung zwischen ihnen?
Sie beseitigt die zeitliche Kopplung, aber beide Services hängen weiterhin vom Format und von der Bedeutung der Nachricht sowie vom Broker selbst ab, daher kann eine Änderung der Nachricht den Empfänger immer noch brechen.
Warum ist DELETE idempotent, obwohl ein zweites DELETE 404 statt 204 liefern kann?
Idempotenz betrifft die beabsichtigte Wirkung auf dem Server, nicht die Antwort: Nach einem oder zwei DELETEs ist die Ressource in jedem Fall weg.
Ein mobiler Client erzeugt bei jeder Wiederholung einen neuen Idempotenzschlüssel. Was geht schief?
Der Server sieht jede Wiederholung als neue Operation, daher kann eine verlorene Antwort mit anschließender Wiederholung trotzdem eine doppelte Bestellung erzeugen; der Schlüssel muss zur logischen Operation gehören, nicht zum HTTP-Versuch.
Warum kann Cursor-Paginierung eine stabile Liste zeigen, wenn neue Bestellungen eintreffen, während der Nutzer blättert, Offset-Paginierung aber möglicherweise nicht?
Der Cursor zeigt direkt hinter einen bestimmten Eintrag, daher verschieben Einfügungen an anderer Stelle die nächste Seite nicht; ein Offset zählt Zeilen, sodass neue Zeilen Einträge über Seitengrenzen schieben und Duplikate oder Lücken verursachen.
Quellen und weiterführende Literatur
Haben Sie eine Frage zu diesem Abschnitt? ArchitectBot, der KI-Lernassistent des Kurses, antwortet zuerst aus dem Kurs.
Fragen: „Unser Bestellservice ruft die Services für Zahlungen, Restaurants und Disposition synchron auf. Welche dieser Aufrufe sollten asynchron werden, und wie halten wir den Kunden auf dem Laufenden?“