Breaking Changes: was zählt und wie man sie ausliefert
9 Min. Lesezeit aktualisiert am
Ein Breaking Change ist eine Änderung, die ein korrekt geschriebener Aufrufer nicht überlebt hätte. Die Definition zählt, weil die meisten Streits darüber, ob etwas “zählt”, eigentlich Streits darüber sind, wer es falsch gehalten hat. Hat ein Aufrufer eurer Dokumentation gefolgt und eure Änderung hat seinen Code kaputt gemacht, war die Änderung ein Breaking Change. Was ihr beabsichtigt habt, hat damit nichts zu tun.
Das ist der ganze Test. Der Rest dieses Artikels ergibt sich daraus: was ihn nicht besteht, was ihn besteht, wie man ein Durchfallen vor dem Merge abfängt, und was zu tun ist, sobald man weiß, dass man einen Breaking Change ausliefert.
Was zählt als Breaking Change?
Wendet den Test auf den Aufrufer an, nicht auf den Diff. Eine Änderung ist ein Breaking Change, wenn ein Aufrufer, der sich nur auf dokumentiertes Verhalten verlassen hat, seinen Code, seine Konfiguration oder seine Daten ändern muss, um weiter zu funktionieren. Ein Feld entfernen, einen Endpunkt umbenennen, Validierung verschärfen, einen Standardwert ändern und den Typ eines Werts ändern qualifizieren sich alle. Ein optionales Feld hinzuzufügen tut das nicht. Einen Bug zu beheben meist auch nicht, mit einer wichtigen Ausnahme unten.
| Änderung | Breaking? | Warum |
|---|---|---|
| Feld, Endpunkt, Flag oder Option entfernen oder umbenennen | Ja | Korrekte Aufrufer referenzieren es |
| Optionales Feld oder neuen Endpunkt hinzufügen | Nein | Bestehende Aufrufe bleiben unverändert |
| Optionale Eingabe verpflichtend machen | Ja | Aufrufe, die sie ausgelassen haben, schlagen jetzt fehl |
| Zuvor akzeptierte Validierung verschärfen | Ja | Eingaben, die funktionierten, werden jetzt abgelehnt |
| Standardwert ändern | Ja | Aufrufer, die ihn nicht gesetzt haben, bekommen neues Verhalten |
| Typ ändern (String zu Zahl, Einzelwert zu Array) | Ja | Parser, die auf den dokumentierten Typ gebaut sind, scheitern |
| Reihenfolge der Schlüssel eines Objekts ändern | Nein | Außer ihr habt die Reihenfolge dokumentiert |
| Bug beheben, auf den Aufrufer sich verlassen haben | Praktisch ja | Siehe Abschnitt über unbeabsichtigte Verträge |
| Ratenlimit oder Größenobergrenze anheben | Nein | Nichts, was funktionierte, hört auf zu funktionieren |
| Ratenlimit oder Größenobergrenze senken | Ja | Traffic, der in Ordnung war, wird jetzt gedrosselt |
| Wortlaut einer Fehlermeldung ändern | Kommt drauf an | Breaking, wenn dokumentiert oder Aufrufer darauf matchen |
Was ist kein Breaking Change?
Eine Änderung ist nicht breaking, wenn jeder Aufruf, der vorher funktionierte, unverändert weiter funktioniert und dasselbe bedeutet. Einen neuen Endpunkt hinzufügen, einen optionalen Anfrageparameter hinzufügen, ein Feld in einer Antwort ergänzen, eine verpflichtende Eingabe optional machen, ein Limit anheben und eine Fehlermeldung verbessern, auf die niemand matcht, bestehen den Test alle. Diese additiven Änderungen können in einem Minor-Release mit einem gewöhnlichen Changelog-Eintrag ausgeliefert werden.
Additive Änderungen brechen Aufrufer trotzdem in drei Situationen. Ein Client, dessen Deserializer unbekannte Felder ablehnt, scheitert am ersten neuen Antwortfeld, also dokumentiert früh, dass Aufrufer Felder ignorieren müssen, die sie nicht kennen. Ein neuer Enum-Wert bricht jeden Aufrufer mit einem erschöpfenden Switch (dazu unten mehr). Und eine Antwort, die wächst, kann einen Aufrufer über ein Größenlimit, einen Timeout oder eine Spaltenbreite schieben, über die er nie nachdenken musste.
Vier Zeilen der Tabelle verdienen einen genaueren Blick, denn dort entstehen die Meinungsverschiedenheiten.
Die vier Breaking Changes, die Teams übersehen
Unbeabsichtigte Verträge. Hat eure API drei Jahre lang dasselbe undokumentierte Feld zurückgegeben, hat ein Aufrufer darauf aufgebaut. Hyrums Gesetz ist die Kurzfassung: Bei genug Nutzern wird jedes beobachtbare Verhalten eures Systems von irgendjemandem abhängen. Deshalb ist “es war ein Bugfix” keine Verteidigung. Der Fix kann korrekt sein und trotzdem ein Breaking Change. Liefert ihn als einen aus.
Verhaltensänderungen ohne Schemaänderung. Das Feld ist noch da, der Typ ist derselbe, und der
Wert bedeutet jetzt etwas anderes. Ein status, der früher active oder inactive war und jetzt
auch suspended zurückgibt, bricht jeden Aufrufer mit einem erschöpfenden Switch. Ein Timestamp,
der von lokaler Zeit auf UTC wechselt, bricht jeden, der die Doku nicht zweimal gelesen hat. Nichts
in einem Diff der OpenAPI-Datei zeigt das.
Verschärfte Validierung. Ihr fangt an, E-Mails ohne TLD abzulehnen, oder abschließende Leerzeichen, oder Namen länger als 80 Zeichen. Jeder Aufrufer, der genau das gesendet hat, bekommt jetzt einen 400er für eine Anfrage, die letzte Woche funktioniert hat. Validierungsänderungen sind die häufigste, die als “Härtungs”-Fix ausgeliefert wird.
Geänderte Standardwerte. Niemand, der den Wert explizit gesetzt hat, merkt etwas. Alle, die es nicht getan haben, also die meisten Aufrufer, bekommen neues Verhalten, ohne eine Zeile geändert zu haben. Ein geänderter Standardwert bricht die Mehrheit eurer Nutzer, genau weil sie die Einstellung nie gesehen haben.
Wie erkennt man einen Breaking Change, bevor er ausgeliefert wird?
Vergleicht in CI den Vertrag im Pull Request mit dem Vertrag im Main-Branch und lasst den Build bei einem Breaking-Unterschied fehlschlagen. Für die meisten Schnittstellenformate gibt es Schema-Diff-Tools, und jedes kennt die Breaking-Regeln seines eigenen Formats:
| Schnittstelle | Tool | Was es vergleicht |
|---|---|---|
| REST (OpenAPI) | oasdiff | Zwei OpenAPI-Specs, mit einem Bericht über Breaking Changes |
| gRPC (Protobuf) | buf breaking | .proto-Dateien, auf Wire- oder Source-Ebene |
| GraphQL | GraphQL Inspector | Zwei Schemas, markiert Breaking und gefährliche Änderungen |
| Rust-Crates | cargo-semver-checks | Die öffentliche API gegen die zuletzt veröffentlichte Version |
| TypeScript-Pakete | API Extractor | Ein eingechecktes Protokoll der öffentlichen API des Pakets |
Diese Tools finden entfernte Felder, umbenannte Operationen und geänderte Typen zuverlässig. Die ersten beiden der vier Arten oben, einen unbeabsichtigten Vertrag und eine Verhaltensänderung, sehen sie nicht, weil beides in keinem Schema auftaucht. Nutzt das Tool, um die offensichtlichen zu stoppen, und die Review-Frage “könnte ein korrekter Aufrufer das bemerken?” für den Rest. Derselbe CI-Job ist ein natürlicher Ort, um einen Changelog-Eintrag zu verlangen, wie in Changelog-Einträge in CI erzwingen beschrieben, und gRPC- und Protobuf-API-Änderungen geht die Fälle auf Wire-Ebene durch.
Wie markiert man einen Breaking Change in einem Commit?
Bei Conventional Commits wird ein Breaking Change
durch ein ! vor dem Doppelpunkt markiert (feat(api)!: remove the legacy export endpoint) oder
durch einen Footer, der mit BREAKING CHANGE: beginnt, gefolgt von einer Beschreibung. Beides
entspricht einer Major-Version. Schreibt den Footer als ersten Entwurf des Changelog-Eintrags und
nennt, wer betroffen ist und was er tun muss. Conventional Commits und der Changelog
behandelt, wie weit die Konvention euch bringt.
Dieselbe Regel gilt für Bibliotheken. Eine entfernte öffentliche Funktion, ein verengter Parametertyp oder ein geänderter Rückgabewert ist unter semantischer Versionierung eine Major-Version. Bibliotheken halten sich nicht immer daran: Eine Studie über 119.879 Maven-Central-Upgrades fand, dass 16,6 % die semantische Versionierung verletzten, aber nur 7,9 % der Client-Projekte betroffen waren, weil die meisten dieser Änderungen Code berührten, den kein Client aufrief. Der Bruch wird beim Aufrufer gemessen.
Wie liefert man einen Breaking Change aus?
Man liefert ihn offen aus, mit einem Datum, mit einem Weg. Die Schritte unten sind in Reihenfolge, und der letzte ist der, den die meisten Teams überspringen: den Betroffenen sagen, dass das, worauf sie gewartet haben, jetzt passiert ist.
- Entscheidet, ob es einer ist. Nutzt den Test oben, nicht den Diff. Sind sich zwei Ingenieure uneinig, ist es ein Breaking Change; die Uneinigkeit belegt, dass ein Aufrufer sich vernünftig auf das alte Verhalten verlassen haben könnte.
- Versioniert ihn. Nach semantischer Versionierung ist ein Breaking Change eine Major-Version. Betreibt ihr eine datierte oder versionierte API, kommt er in eine neue Version, und die alte funktioniert bis zu einem genannten Datum weiter. Könnt ihr nicht versionieren, liefert ihr keinen Breaking Change aus, sondern einen Ausfall mit Changelog-Eintrag. Welches Schema die Version trägt, ist Thema von API-Versionierung: Best Practices.
- Schreibt den Eintrag, bevor der Code gemergt wird. Der Eintrag hat eine feste Form: was sich ändert, wen es betrifft, was sie tun müssen, und bis wann. Könnt ihr nicht alle vier ausfüllen, ist die Änderung nicht bereit. Die Release-Notes-Vorlage stellt diese Einträge genau deswegen mit einem Datum statt einer Versionsnummer nach vorn.
- Gebt eine Frist, keine Release-Nummer. “Entfernt in v5” bedeutet nichts für jemanden, der eure Releases nicht verfolgt. “Funktioniert ab 1. November 2026 nicht mehr” bedeutet für alle dasselbe.
- Liefert die Migration mit. Ein Codebeispiel des alten Aufrufs neben dem neuen. Ist es eine Umbenennung, nennt beide Namen im selben Satz. Ist es ein entferntes Feld, sagt, wohin die Daten gegangen sind.
- Kündigt es überall an, wo das alte Verhalten dokumentiert war. Der Changelog, die Doku-Seite zum Endpunkt, die Release Notes des SDK, und der Deprecation-Header in der Antwort, falls vorhanden. An einem Ort angekündigt heißt: den Leuten angekündigt, die zufällig dort nachgeschaut haben.
- Schließt den Loop. Hat eine Kundin um die Änderung gebeten oder den Bug gemeldet, der dazu führte, sagt ihr Bescheid, wenn es ausgeliefert wird. Das ist der Schritt, der aus etwas, das euren Nutzern angetan wurde, etwas macht, das mit ihnen gemacht wurde.
Wie sieht ein guter Eintrag für einen Breaking Change aus?
Ein guter Eintrag nennt den betroffenen Aufrufer in der ersten Zeile, gibt das Datum an und enthält den Fix. Hier einer für den Fall verschärfter Validierung, in der Form, die wir nutzen:
E-Mail-Adressen ohne Domain werden ab 1. November 2026 abgelehnt.
POST /usersundPATCH /users/:idakzeptieren derzeitalice@localhost. Ab dem 1. November geben sie400 invalid_emailzurück. Betrifft jede Integration, die Nutzerkonten aus internen Verzeichnissen anlegt. Migration: eine vollständig qualifizierte Adresse senden, oder das Feld auslassen und später setzen. Keine Änderung nötig, falls eure Adressen schon eine Domain haben, was auf 99,4 % der dieses Jahr angelegten Konten zutrifft.
Wo dieser Hinweis hingehört, und was sonst daneben stehen sollte, behandelt der API-Changelog.
Der Prozentsatz am Ende ist keine Dekoration. Er sagt der Leserin, ob sie sich Sorgen machen muss, was die Frage ist, mit der sie den Eintrag geöffnet hat.
Warum sie nicht einfach vermeiden?
Weil die Alternative schlimmer ist. Eine API, die nie etwas kaputt macht, sammelt jeden Fehler an, den sie je gemacht hat: das falsch benannte Feld, den falschen Standardwert, den Timestamp in lokaler Zeit. Jeder davon ist eine Steuer auf jeden neuen Aufrufer, für immer, um Aufrufer zu schützen, die an einem Nachmittag hätten migrieren können. Die Teams mit dem besten Ruf für Stabilität brechen Dinge selten, nach Plan, mit einem Migrationsweg und einer Warnung, die die Betroffenen erreicht hat.
Die Mechanik dieser Warnung ist Thema des begleitenden Artikels über eine API abkündigen. Der Eintrag, der es ankündigt, wird auf dieselbe Art entworfen wie jeder andere Eintrag im Changelog-Feed: aus dem gemergten Pull Request, für einen Menschen zurückgehalten, dann veröffentlicht dort, wo die betroffenen Aufrufer schon lesen.
FAQ
Was ist der Unterschied zwischen einem Breaking und einem Non-Breaking Change? Ein Breaking Change zwingt einen korrekten Aufrufer, Code, Konfiguration oder Daten zu ändern, um weiter zu funktionieren. Ein Non-Breaking Change lässt jeden bestehenden Aufruf mit derselben Bedeutung weiter funktionieren, weshalb Ergänzungen meist sicher sind und Entfernungen, Umbenennungen und verschärfte Regeln meist nicht.
Zählt das Hinzufügen eines verpflichtenden Felds? Ja. Jeder bestehende Aufruf lässt es aus, also schlägt jeder bestehende Aufruf jetzt fehl. Fügt es als optional mit einem sinnvollen Standardwert hinzu, oder versioniert den Endpunkt.
Zählt ein Bugfix? Kann sein. Haben sich Aufrufer auf das fehlerhafte Verhalten verlassen, bricht das Beheben sie, egal was die Doku sagte. Behandelt jeden Fix, der beobachtbare Ausgabe ändert, als Breaking Change, außer ihr könnt zeigen, dass niemand sich darauf verlassen hat.
Gilt semantische Versionierung für eine Web-API? Die Regel gilt: Breaking Changes bekommen eine neue Major-Version, und die alte funktioniert für eine genannte Zeitspanne weiter. Die Nummer lebt oft in der URL oder einem Datums-Header statt in einer Paketversion.
Wie viel Vorlauf ist genug? Genug, damit ein Aufrufer die Ankündigung findet und die Arbeit erledigt. Neunzig Tage sind eine übliche Untergrenze für öffentliche APIs; länger für alles, was in Code steckt, der an Endnutzer ausgeliefert wird und sich nicht remote aktualisieren lässt.
Die technischen Aussagen in diesem Artikel wurden nicht unabhängig geprüft. Wenn etwas nicht stimmt, sagen Sie es uns, und wir korrigieren es.