← Zurück zum Journal
GRUNDPFEILERData Engineering

Data Contracts — das Schema ist eine gemeinsame Schnittstelle, kein privates Detail

Cordata-Perspektive·Zuletzt aktualisiert 11. Oktober 2026

Data ContractsData EngineeringSchema EvolutionData QualitySoftware DevelopmentOpenlineage

Das Release, das die Montagszahl kaputt gemacht hat

Eine Spalte wird in einem gewöhnlichen Freitags-Release umbenannt. Jeder Test, den das Anwendungsteam besitzt, läuft durch, weil nichts in diesem Repository weiß, dass die Spalte anderswo gelesen wird. Am Montag fällt die Pipeline, die das Finanz-Dashboard speist, entweder laut aus oder — schlimmer — sie läuft durch und veröffentlicht eine Zahl, die still falsch ist. Jemand im Finanzbereich stutzt über den Wert, jemand aus dem Data-Team verbringt den Vormittag damit, ihn zurückzuverfolgen, und das Anwendungsteam ist ehrlich überrascht, weil nichts in seinem eigenen Verantwortungsbereich ihm gesagt hat, dass jemand von diesem Feld abhängt.

Die meisten Datenvorfälle, die ein Dashboard erreichen, beginnen so, und sie kommen aus zwei Fehlern, die einander nähren.

Das private Schema. Das Anwendungsteam — im Folgenden der Producer, gleich ob es eine Tabelle, einen Event-Stream oder ein API-Payload veröffentlicht — behandelt sein Schema als internen Zustand, jederzeit änderbar. Das ist eine vernünftige Grundannahme: Ein Schema ist ein Implementierungsdetail eines Dienstes, und nichts im Build dieses Teams sagt etwas anderes.

Die absorbierte Änderung. Das Data-Team — der Consumer, zusammen mit jeder Pipeline, jedem Modell und jedem Dashboard dahinter — nimmt hin, was ankommt, und arbeitet stromabwärts darum herum: hier eine Typkonvertierung, dort eine Umbenennung in der Transformationsschicht, Schema Evolution eingeschaltet, Drift-Alarme in den eigenen Kanal geleitet. Lokal ist auch das vernünftig, denn die Dashboards bleiben grün und mit niemandem stromaufwärts muss diskutiert werden.

Die üblichen Erklärungen — schlechte Kommunikation, fehlende Dokumentation, das falsche Ingestion-Werkzeug — treffen zu und sind Folgen dieser beiden. Dokumentation lässt keinen Build scheitern.

Warum das heute mehr kostet als früher

Eine Spalte speiste früher einen Report, gelesen von Menschen, denen aufgefallen wäre, wenn sie seltsam wird. Heute speist sie Features, Embeddings und KI-Anwendungsfälle, und die scheitern still statt laut: Ein Modell wirft bei einer umbenannten Spalte keine Exception, es scort auf einem Default und liefert eine plausible Zahl. Ein Modell, dessen Eingaben ihre Bedeutung geändert haben, ohne dass es jemand bemerkt hat, ist ein Modell, das niemand belegen kann — und da hört das auf, eine Unannehmlichkeit im Data Engineering zu sein, und wird zu einem Governance-Problem.

Es untergräbt auch die Nachweise, die darauf aufbauen. Der Data-Governance-Grundpfeiler argumentiert, dass Governance-Artefakte aus dem erzeugt werden sollten, was Systeme tatsächlich tun, statt daneben gepflegt zu werden. Ein aus Lineage erzeugtes Verarbeitungsverzeichnis beschreibt, was die Pipeline gelesen hat. Hat die Anwendung geändert, was ein Feld bedeutet, ist dieses Verzeichnis selbstbewusst und automatisch falsch.

Regulierung erreicht das in einzelnen Branchen statt überall, und die genaue Fassung dieser Aussage ist kurz: Kein allgemeines Gesetz verlangt Data Contracts. Was es gibt, ist enger gefasst und wird weiter unten präzise benannt.

Jeder der beiden Fehler nährt den anderen

Das private Schema und die absorbierte Änderung sind keine unabhängigen Fehler. Jede Änderung, die der Consumer absorbiert, lehrt den Producer, dass eine Schemaänderung nichts kostet, weil die Kosten dort anfallen, wo der Producer sie nie sieht: in der Pipeline eines anderen Teams, in einem Repository, dessen Build ohnehin grün war. Also bleibt das Schema privat, und die nächste Änderung kommt mit so wenig Vorwarnung wie die letzte und muss ihrerseits absorbiert werden.

Der Producer ändert das Schema weiter, weil sein eigener Build nie fehlschlägt, und der Consumer absorbiert die Änderung weiter, weil nichts, was ihm gehört, diesen Build zum Fehlschlagen bringen kann.

Der Markt verkauft die Hälften getrennt. Spezifikationsformate und Registries adressieren die Papierseite des Producers, Observability und Drift-Erkennung den Schaden auf der Consumer-Seite. Keines von beiden ändert, was im Moment des Releases passiert, und das ist der einzige Moment, in dem die Kosten noch klein sind.

Was ein Data Contract ist und was nicht

Ein Data Contract ist eine Vereinbarung zwischen einem Producer und benannten Consumern über Form und Bedeutung von Daten und darüber, wie beide Seiten sie ändern dürfen — durchgesetzt dort, wo die Änderung gemacht wird.

Es gibt gute Vorarbeit, auf der das aufbaut, und sie gehört genau benannt statt vage erwähnt:

  • Der Open Data Contract Standard (v3.2.0, betreut von Bitol, das im Juli 2026 in der AI & Data Foundation der Linux Foundation graduiert ist) definiert über fünfzehn Abschnitte, was ein Contract-Dokument enthält — Schema, Qualität, Service Levels, Rollen, Support. Er sagt bewusst nichts darüber, wo der Contract durchgesetzt wird.
  • Die Data Contract Specification ist zugunsten dieses Standards abgekündigt und sollte nicht als aktuell zitiert werden. Die Data Contract CLI, gepflegt von Entropy Data, läuft auf ODCS und liefert die Kommandos test, ci und breaking, gedacht für Pull Requests.
  • Andrew Jones' Driving Data Quality with Data Contracts (Packt, 2023) führt die organisatorische Begründung und argumentiert, dass der Contract dem Producer gehört. Das Buch behandelt an vier Stellen auch CI-Prüfungen, darunter „Continuous Integration (CI) checks, giving both the generators and consumers confidence that their code is using the data models correctly as they develop their services“.
  • dbt Model Contracts setzen die Form zur Build-Zeit für die eigenen Modelle durch und erkennen Breaking Changes im Vergleich mit einem früheren Projektstand — das Muster, das in CI läuft. Bei nicht versionierten Modellen warnt dbt, statt fehlzuschlagen, was man wissen sollte, bevor man sich darauf verlässt.

Durchsetzung im Build der erzeugenden Anwendung ist damit keine neue Idee, und dieser Grundpfeiler beansprucht sie nicht. Adrian Kreuziger hat es im Oktober 2022 in An Engineer's Guide to Data Contracts, geschrieben für Chad Sandersons Newsletter, klar gesagt: „Data contracts must be enforced at the producer level. If there's nothing enforcing a contract on the producer side, you don't have a contract.“

Drei Dinge, mit denen ein Contract häufig verwechselt wird:

  • Keine Spezifikationsdatei. Ein Dokument ist der Text des Contracts. Ohne eine Prüfung dort, wo die Änderung gemacht wird, ist es eine Wunschliste.
  • Keine Schema Registry. Eine Registry hält fest, welche Versionen eine Tabelle hatte. Ein Contract hält zusätzlich fest, wer sie liest — dass fraud_raw.transactions in Version 7 von einer Fraud-Pipeline und einer Finance-Pipeline gelesen wird, sodass an einer entfernten Spalte zwei Teamnamen hängen, bevor jemand ausliefert. Diese Tabelle und diese beiden Consumer sind das Beispiel, mit dem der Rest dieses Grundpfeilers arbeitet — erst als Deskriptor, dann als Ausgabe der Prüfung selbst.
  • Kein Service Level Agreement. Zusagen zu Aktualität und Verfügbarkeit sind wichtig, und sie beschreiben die Ausgabeseite — ein anderer Contract als die Vereinbarung über Form und Bedeutung, um die es hier geht.

Beide Seiten, auf einer Seite

Die Producer-Seite ist ein Satz von vier Praktiken und kein Programm, und jede davon existiert, um einen bestimmten Fehler unmöglich zu machen.

Jede veröffentlichte Form trägt eine Version, damit ein Consumer sagen kann, gegen welche Form er geschrieben wurde, statt zu hoffen, dass die aktuelle noch passt. Ein Breaking Change erzeugt eine neue Version statt einer Änderung an der bestehenden — brechend heißt hier: eine entfernte Spalte, eine Spalte mit geändertem Typ oder eine Spalte, die jetzt null sein darf, wo die alte Version das Gegenteil zugesagt hat. Das sind die drei Arten, auf die die Abfrage eines Consumers aufhört zu bedeuten, was sie bedeutet hat. Additive Änderungen brauchen keine neue Version, weil ein Consumer, der die alten Spalten liest, von einer Spalte, die er nie selektiert hat, nicht betroffen ist.

Eine abgekündigte Version bleibt bis zu einem genannten Datum lesbar. Das Datum macht die Abkündigung erst real: Ohne Datum verschwindet die alte Version entweder an dem Tag, an dem der Producer aufhört, an sie zu denken, oder sie überlebt Jahre, weil niemand sagen kann, wann Schluss sein soll. Mit Datum wird das Umstellen der festgeschriebenen Versionen zu geplanter Arbeit der konsumierenden Teams statt zu einer Unterbrechung.

Das Release prüft vor dem Ausliefern gegen die Consumer und lässt den Build scheitern, wenn einer von ihnen brechen würde. Das ist die eine der vier Praktiken, die nach einem Vorschlag klingt, deshalb sei gesagt, dass sie keiner ist: Die Data Contract CLI liefert ein breaking-Kommando für Pull Requests, dbt erkennt Breaking Changes an einem Modell mit Contract im Vergleich mit einem früheren Projektstand, und beide sind oben zitiert. Der Unterschied zu dem, was hier folgt, ist nicht die Prüfung, sondern woher sie ihre Liste der Consumer bekommt — und der Abschnitt darüber, was die Runtime durchsetzt, zeigt unsere im Lauf, an einer Tabelle mit zwei davon.

Wo die eigene Datenbank der Anwendung die Quelle ist, ist die Schnittstelle eine Outbox-Tabelle oder ein veröffentlichtes Event statt der internen Tabellen, damit der Producer die Freiheit behält, alles dahinter umzubauen. Der Sinn eines Contracts ist es, eine Schnittstelle zu haben, nicht die Implementierung einzufrieren.

Die Consumer-Seite sind zwei Praktiken. Jeder Consumer deklariert die Version, die er liest, und genau das bringt ihn auf die Liste, die die Prüfung des Producers heranzieht. Und er bleibt bei einer Abweichung stehen, statt sie zu absorbieren — das Scheitern ist der ganze Wert, denn eine Pipeline, die eine geänderte Spalte stillschweigend aufnimmt, ist der Weg, auf dem eine falsche Zahl ein Dashboard erreicht. Drift-Erkennung bleibt hinter beidem bestehen, als Auffangnetz für Quellen, die sich gar nicht unter Contract bringen lassen.

Beide Seiten sind technologieneutral. Outbox und Change Data Capture sind hier Muster, keine Produkte.

Die Consumer-Liste wird deklariert, nicht gepflegt

Jeder Contract hat zwei Parteien, und die unangenehme ist der Consumer. Ein Contract-Dokument kann die Form einer Tabelle genau beschreiben und trotzdem falsch darin sein, wer sie liest, weil die Liste der Consumer von Hand getippt ist: Sie veraltet, sobald jemand eine Pipeline hinzufügt, der von dem Dokument nichts wusste. Ob eine Prüfung auf der Producer-Seite den Aufwand wert ist, entscheidet sich daran, woher ihre Liste der Consumer kommt.

Die Alternative ist, die Liste gar nicht mehr zu pflegen. Jede Pipeline hier deklariert ohnehin die Version jeder Tabelle, die sie liest, weil der Executor sonst nicht startet — die Festschreibung aus Teil 1 § 4, jetzt unter einem Namen, der sagt, welche Seite sie bindet.

Die Form unten unterscheidet sich bewusst von den Deskriptoren, die Teil 1 § 2 abdruckt. Jene sind cordata.tech/v1, wo ein einzelner contract:-Block auf oberster Ebene die Freshness-SLA und die LF-Tags hielt und die Festschreibung der Eingabe lose als schema_version in source: stand. Dies ist v2, das jeder Seite einen eigenen Contract gibt — und die veröffentlichten v1-Deskriptoren bleiben exakt so, wie sie sind, und laufen weiter, weil der Executor das Modell zum Parsen aus apiVersion wählt, sodass v2 neben v1 landet statt an seiner Stelle.

apiVersion: cordata.tech/v2
 
source:
  database: fraud_raw
  table: transactions
  contract:                          # wovon diese Pipeline stromaufwärts abhängt
    schema_version: 7
 
target:
  database: finance_curated
  table: merchant_settlement_daily
  contract:                          # was sie stromabwärts zusagt
    freshness_sla: PT6H

Zwei Blöcke eines echten Deskriptors, auf die Contracts gekürzt: Metadaten, Steps, Expectations und Processing darum herum sind weggelassen, ebenso die Speicherdetails darin — kind, location, der Partitionsschlüssel und die LF-Tags, die der Target-Contract ebenfalls trägt.

Diese Festschreibung ist keine Dokumentation. Sie ist das, worauf die Pipeline läuft, und sie zu ändern ändert den Lauf. Damit ist die Menge der Deskriptoren, die fraud_raw.transactions festschreiben, dessen Consumer-Liste, und niemand muss daran denken, sie zu aktualisieren: Eine Pipeline kommt in dem Moment auf die Liste, in dem ihr Deskriptor es tut, und verlässt sie mit ihm.

Die Benennung ist das Argument dieses Grundpfeilers im Kleinen. Version 1 hatte einen contract:-Block am Kopf der Datei, und alles darin beschrieb, was die Pipeline stromabwärts zusagt; wovon die Pipeline stromaufwärts abhing, war eine nackte schema_version in source:, ohne ein Wort dafür. Jeder spezifiziert, was er veröffentlicht, und niemand spezifiziert, was er konsumiert — bis in den Wortschatz hinein.

Das Argument des Data-Governance-Grundpfeilers — dass ein Governance-Artefakt aus dem erzeugt werden sollte, was Systeme tatsächlich tun, statt daneben gepflegt zu werden — greift hier einen Schritt früher im Leben einer Pipeline. Dort wird das Verarbeitungsverzeichnis aus den Lineage-Events erzeugt, die eine Pipeline nach ihrem Lauf aussendet; hier entsteht die Consumer-Liste aus dem, was ihr Deskriptor deklariert, bevor sie überhaupt läuft.

Es gibt zwei Grenzen, und die erste ist groß. Deklaration zuerst sieht nur die Consumer, die deklarieren, und Ad-hoc-SQL, Notebooks und Dashboards lesen Tabellen, ohne irgendetwas festzuschreiben, sodass kein Deskriptor sie findet — oft genau die Leser, die eine Umbenennung zuerst trifft.

Der Rückfall ist, die nicht deklarierten Leser aus dem zu rekonstruieren, was die Lesezugriffe protokolliert, und was das ist, hängt davon ab, wie die Tabelle gelesen wird. Steht eine Query Engine davor, gibt es eine View zum Abfragen — ACCOUNT_USAGE.ACCESS_HISTORY in Snowflake, INFORMATION_SCHEMA.JOBS in BigQuery oder die Datenzugriffs-Events von Lake Formation in CloudTrail, wo es die Credentials vermittelt. Steht nichts davor, was der Fall einer Iceberg-Tabelle auf S3 ist, die ein Spark-Job mit eigener Rolle liest, liegt die einzige Aufzeichnung beim Bucket: CloudTrail-Data-Events oder Server-Access-Logs auf dem Präfix der Tabelle, beides standardmäßig ausgeschaltet, und beides nennt einen IAM-Principal, der erst noch einem Team zugeordnet werden muss. Die Operation ist in beiden Fällen dieselbe — auf die Tabelle filtern und die Deskriptoren abziehen, die sie ohnehin festschreiben —, und was übrig bleibt, ist eine Menge von Teams, die man ansprechen sollte, statt einer zweiten Liste, die aktuell gehalten werden muss, denn diese Logs haben Aufbewahrungsfristen und berichten, wer die Tabelle innerhalb einer solchen Frist gelesen hat, und nicht, wer von ihr abhängt.

Die zweite Grenze ist die, die Jones benennt. Eine Versionsprüfung vergleicht Form, nicht Bedeutung. Eine Spalte, die ihren Namen und ihren Typ behält, während sich ihre Definition darunter verschiebt — ein Betrag, der plötzlich ohne Steuer gemeint ist, ein Zeitstempel, der die Zeitzone wechselt —, kommt durch alles oben Beschriebene hindurch, und die Pipeline läuft weiter gegen Daten, die nicht mehr bedeuten, was sie bedeutet haben. Was dieser Grundpfeiler anbietet, ist, dass auch Bedeutung deklariert wird: Der Deskriptor trägt für das Verarbeitungsverzeichnis bereits purpose und legal_basis, und Einheiten und Definitionen gehören daneben. Das ist nicht gebaut, und das gehört klar gesagt statt angedeutet. Und deklarierte Bedeutung ist nur so gut wie derjenige, der sie deklariert — da hört ein Contract auf, ein technischer Mechanismus zu sein, und wird zu zwei Teams, die sich einigen, was eine Zahl ist.

Was jede Seite schuldet

Welche Seite welchen Schritt besitzt, und wo eine Schemaänderung auffälltDrei Bahnen. Die Consumer-Bahn enthält die Pipelines, die die Tabelle lesen: Jeder Deskriptor schreibt unter source.contract.schema_version eine Version fest, und ein Pipeline-Lauf löst dieselbe Festschreibung gegen den Katalog auf und erzeugt einen Drift-Fehler, der Spalte und Release nennt, wenn die Festschreibung veraltet ist. Die Producer-Bahn enthält das Repository der Anwendung, wo ein Pull Request pipeline_runtime.consumers ausführt, das Kommando des Plattformteams, laufend im Build der Anwendung selbst; eine entfernte, umtypisierte oder nullable gewordene Spalte lässt diesen Build scheitern und benennt die Consumer und ihre Owner. Dem Kommando werden Pfade zu den Repositories übergeben, die jene Deskriptoren enthalten; es parst jeden Deskriptor darunter und behält die, deren Quelle die Tabelle nennt, und das ist die eine Kante, die zwischen den Bahnen nach oben führt. Die Plattform-Bahn enthält den Katalog, wo eine kompatible Änderung eine neue Tabellenversion mit Producer und Release veröffentlicht, und der Lauf des Consumers liest diese Version vor seiner ersten Zeile.

Plattform · der Katalog

kompatibel

vor der ersten gelesenen Zeile aufgelöst

aus den Domain-Repositories gescannt

Producer-Seite · das Repository der Anwendung

Spalte entfernt · umtypisiert · jetzt nullable

Pull Request
in der erzeugenden Anwendung

pipeline_runtime.consumers
das Kommando der Plattform, in der CI der Anwendung

Build scheitert
Consumer und Owner benannt

Consumer-Seite · die Pipelines, die die Tabelle lesen

dieselbe Festschreibung, zur Laufzeit

Festschreibung veraltet

Deskriptoren
source.contract.schema_version

Pipeline-Lauf
gegen den Katalog aufgelöst

Drift-Fehler
nennt Spalte und Release

Neue Tabellenversion
Producer + Release erfasst

Das Diagramm im Klartext. Jede Bahn enthält die Schritte, die auf einer Seite stattfinden, und die zwei Kanten, die zwischen den Bahnen verlaufen, tragen den Entwurf. Eine Bahn sagt, wo ein Schritt läuft, und nicht, wer ihn pflegt; deshalb steht die zweite Pflicht der Plattform, das Kommando selbst, in der Bahn des Producers: Die Plattform liefert es aus, der Build der Anwendung führt es aus. Die Prüfung läuft im Pull Request der erzeugenden Anwendung, in der Bahn des Producers, und sie kommt durch Scannen zu ihrer Liste der Consumer statt durch Nachschlagen: Das Kommando nimmt die Version, die die Anwendung zu veröffentlichen vorhat, dazu einen oder mehrere Pfade zu den Repositories mit den Deskriptoren, parst jeden Deskriptor unter diesen Pfaden und behält die, deren Quelle die Tabelle nennt. Die Liste sind die Festschreibungen selbst, also ist kein Consumer irgendwo registriert, und eine morgen hinzugefügte Pipeline steht in dem Moment darauf, in dem ihr Deskriptor existiert. Nur drei Änderungen lassen diesen Build scheitern, und die Kante sagt welche: eine entfernte Spalte, eine umtypisierte Spalte oder eine Spalte, die jetzt null ankommen darf. Eine kompatible Änderung veröffentlicht stattdessen eine neue Tabellenversion im Katalog, mit der Angabe, wer sie veröffentlicht hat und aus welchem Release, und gegen diese Version löst ein Pipeline-Lauf später seine Festschreibung auf — was an der Prüfung vorbeikam, scheitert also weiterhin beim Consumer, mit Spalte und Release im Klartext. Die gepunktete Kante von den Deskriptoren zum Lauf, dieselbe Festschreibung, zur Laufzeit, ist eine Deklaration, die zwei Aufgaben erfüllt: eine einzige festgeschriebene Version, einmal gelesen vom Build des Producers und einmal vom Executor des Consumers.

Eine Pflicht je Bahn, und jede klein genug, um ein Team darauf festzulegen:

  • Der Producer schuldet eine versionierte Schnittstelle, eine Release-Referenz an jeder Version und die Prüfung, die im eigenen Build läuft. Das Team, das die Daten ändern kann, ist das Team, dessen Build scheitert.
  • Der Consumer schuldet eine Deklaration — die Version, die er liest, festgeschrieben im Deskriptor — und ein lautes Scheitern, wenn Wirklichkeit und Festschreibung auseinandergehen.
  • Die Plattform schuldet das Kommando, das aus Deklarationen eine Consumer-Liste und ein Urteil macht, lauffähig in der CI jedes Repositories, und einen Katalog, der festhält, welches Release welche Version veröffentlicht hat.

Die Pflicht des Producers hat eine Voraussetzung, die genannt gehört, weil sie der Teil ist, der Einrichtung braucht. Das Kommando gehört dem Plattformteam, das es als Paket ausliefert und am Laufen hält, und es läuft im Build der erzeugenden Anwendung wie jeder andere Build-Schritt. Es entdeckt nichts: Es scannt die Pfade, die ihm übergeben werden, also braucht der Build der Anwendung die Domain-Repositories neben dem eigenen — in einem Mono-Repo ohnehin gegeben, und ein Klon-Schritt mit Lesezugriff vor der Prüfung, wo die Domains eigene Repositories haben.

Damit bleibt eine Liste von Hand gepflegt, und die gehört benannt, statt sie unter der Formulierung nichts zu registrieren verschwinden zu lassen: die Menge der zu scannenden Repositories. Sie bewegt sich weit langsamer als ein Consumer-Register, trägt einen Eintrag je Domain-Repository statt einen je Pipeline, ändert sich, wenn eine Domain entsteht, und nicht, wenn eine Pipeline hinzukommt, und sie liegt in der CI-Konfiguration der erzeugenden Anwendung, wo dieses Team sie sieht. Auch der Fehlermodus gehört dazu, denn er ist leise: Ein Repository, das niemand eingetragen hat, wird schlicht nicht gescannt, und seine Pipelines fehlen im Urteil, statt als unbekannt gemeldet zu werden. Wo die Domains in einer Organisation liegen, ist es der Weg gegen das Veralten, diese Liste aus den Repositories der Organisation zu erzeugen. Dass die Domains in einer Organisation liegen, ist die Annahme unter all dem, denn die Prüfung liest Deskriptoren, die der erzeugende Build öffnen kann, und der Lauf löst gegen einen Katalog auf, den jemand für beide Seiten betreibt; der Abschnitt darüber, wann dies die falsche Antwort ist, sagt, welche Hälfte übrig bleibt, wenn Daten über diese Grenze hinaus veröffentlicht werden.

Den Build brechen, nicht das Dashboard

Cordatas Position ist ein Satz, und es ist der Satz, den man in die nächste Diskussion über Release-Geschwindigkeit mitnimmt:

Eine brechende Schemaänderung sollte den Build der Anwendung scheitern lassen, die sie gemacht hat, den Lauf des Consumers nur dann erreichen, wenn nichts in diesem Build sie hätte fangen können, und ein Dashboard nur dann, wenn beides versagt hat.

Diese Position teilen Kreuziger, Sanderson und Jones; was dieser Grundpfeiler hinzufügt, ist, woher die Prüfung ihre Liste der Consumer bekommt, und lauffähiger Code für beide Hälften.

Wo eine brechende Änderung auffällt, entscheidet, wer sie bezahlt. Gefangen im Build des Producers kostet sie eine rote Pipeline und ein Gespräch mit dem Team, das in der Ausgabe benannt ist, bevor die Änderung irgendwo existiert — und die Leute, die diesen Fehler lesen, sind die Leute, die sie ändern können. Einen Schritt später, beim nächsten Lauf des Consumers, ist die Änderung bereits ausgeliefert, also bleibt eine Pipeline stehen und jemand verbringt einen Vormittag damit, das Warum zu klären: späte Daten statt falscher Daten, die gute Variante eines schlechten Tages. Das Dashboard ist die Stelle, an der es teuer wird, weil dort gar nichts gescheitert ist: Die Zahl ist falsch, jemand zweifelt an ihr, und zu reparieren ist danach das Vertrauen in die Zahl und nicht eine Spalte.

Das ist kein Argument dafür, Schemata einzufrieren. Eine versionierte Änderung mit Abkündigungsfenster geht so schnell raus wie jedes andere Release, und die Prüfung benennt genau, mit welchen Teams zu reden ist. Die Reibung verschiebt sich in den Moment, in dem die Änderung am billigsten zu besprechen ist, und das ist, bevor sie in Produktion existiert.

Die Prüfung des Consumers bleibt bestehen, auch wenn der Build des Producers grün ist, und nicht als Versicherung gegen ein Versagen der ersten Prüfung. Eine hinzugefügte Spalte kann den Build des Producers nicht scheitern lassen, weil kein Consumer eine Version festschreiben kann, die es noch nicht gibt; eine Pipeline, die die neue Form braucht, erfährt davon also weiterhin beim eigenen nächsten Lauf. Und Leser, die nichts deklarieren, sind für den Scan konstruktionsbedingt unsichtbar. Die beiden Prüfungen beantworten verschiedene Fragen, und deshalb behält der Entwurf beide, statt die zweite als Rückfall für die erste zu behandeln.

Was Cordata nicht verkauft

Cordata verkauft weder (a) Data-Contract-Programme, die das Data-Team schreibt und die Anwendungsteams nie sehen, noch (b) Drift-Behandlung stromabwärts, verkauft als die Lösung für Schemaänderungen stromaufwärts statt als das Auffangnetz dahinter. Beide Absagen kommen aus Erfahrung.

Ein Contract, den nur eine Partei unterschrieben hat, ist eine Wunschliste. Das Data-Team schreibt in gutem Glauben auf, welche Spalten und Typen es von jeder Quelle erwartet, veröffentlicht die Spezifikationen in einer Registry und verdrahtet eine Prüfung in den eigenen Pipelines. Nichts davon läuft dort, wo die Änderung gemacht wird. Ein Anwendungsteam benennt in einem gewöhnlichen Release eine Spalte um, seine CI läuft durch, weil nichts in dieser CI weiß, dass die Spalte konsumiert wird, und das erste System, das von dem Contract erfährt, ist die Pipeline, die gerade an ihm zerbrochen ist — genau die Lage, gegen die der Contract gekauft wurde.

Eine Pipeline, die Änderungen von stromaufwärts absorbiert, lehrt das Team stromaufwärts, dass seine Änderungen nichts kosten — was, von innerhalb dieses Repositories betrachtet, stimmt. Automatische Schema Evolution, Typkonvertierung und Drift-Alarme in den Kanal des Data-Teams halten allesamt die Dashboards grün, und jedes davon verschiebt die Kosten einer brechenden Änderung von dem Team, das sie gemacht hat, zu dem Team, das sie nicht gemacht hat. Über ein Jahr driftet die Quelle weiter, weil nichts zurückdrückt, und die Behelfslösungen stromabwärts werden zu der undokumentierten Logik, von der die Zahlen tatsächlich abhängen.

Was Cordata stattdessen verkauft: Contracts, durchgesetzt im Release-Prozess der erzeugenden Anwendung, sodass eine Änderung, die einen registrierten Consumer brechen würde, den Build der Anwendung scheitern lässt statt den nächsten Lauf der Pipeline, und Schemaversionen, die das Release tragen, das sie veröffentlicht hat, sodass der Fehler das Team benennt, das ihn beheben kann, wenn doch etwas durchkommt. Das holt die Anwendungsteams ab der ersten Woche in das Projekt, statt sie am Ende zu informieren. Wir sind bereit, den Auftrag zu verlieren, wenn ein Kunde Contracts ohne diese Teams im Raum will — ein Contract, dem der Producer nie zugestimmt hat, wird nur gegen die Partei durchgesetzt, die die Daten nicht ändern kann.

Wohin Regulierung tatsächlich reicht

Kein allgemeines Gesetz verlangt Data Contracts. Vier Dinge kommen nahe genug heran, um benannt zu werden, und jedes erreicht ein bestimmtes Publikum:

Anknüpfungspunkt Wen es erreicht Was es verlangt, wobei ein Contract hilft
DSGVO Art. 5 Abs. 1 lit. d — personenbezogene Daten müssen „sachlich richtig und erforderlichenfalls auf dem neuesten Stand“ sein alle, die personenbezogene Daten verarbeiten ein aus Lineage erzeugtes Verarbeitungsverzeichnis stimmt nur, wenn sich die Bedeutung eines Feldes stromaufwärts nicht still geändert hat
Der EZB-Leitfaden zur Risikodatenaggregation von 2024, aufbauend auf BCBS 239 bedeutende Institute unter EZB-Aufsicht, worunter die großen deutschen und österreichischen Banken fallen Data Owner, die Qualitätskontrollen „in alignment with data users (or consumers)“ definieren, und Lineage auf Attributebene ab der Datenerfassung
DORA Art. 8 Abs. 1, 4 und 6; Art. 9 Abs. 4 lit. e Finanzunternehmen Informations- und IKT-Assets und ihre Abhängigkeiten untereinander dokumentieren; Änderungen an IKT-Systemen, die erfasst, getestet und genehmigt sind
FINMA-Rundschreiben 2023/1, kritische Daten Schweizer Banken kritische Daten identifizieren und über ihren Lebenszyklus steuern — eine lockerere Passung, ausgerichtet auf Vertraulichkeit, Integrität und Verfügbarkeit

Zwei Hinweise für alle, die das nachprüfen. Österreich hat kein eigenes nationales Instrument: Der Anhang des EZB-Leitfadens listet auf, was jeder Mitgliedstaat hat, und Österreich erscheint dort mit zwei allgemeinen Risikomanagement-Bestimmungen, wo Belgien, Spanien und Portugal eigens dafür geschriebene Rundschreiben haben. Und derselbe Anhang schreibt Deutschland weiterhin ein MaRisk-Modul zu, das die Novelle vom Juni 2026 gestrichen hat — eine Erinnerung daran, dass ein regulatorisches Zitat ein Haltbarkeitsdatum hat.

Wo KI in der Contract-Arbeit hingehört

KI hilft beim Text um einen Contract herum und nicht beim Urteil. Sie kann einen ersten Contract aus einem bestehenden Schema und den Deskriptoren entwerfen, die es bereits lesen, für einen Pull Request zusammenfassen, welche Consumer eine vorgeschlagene Änderung treffen würde und was jeder von ihnen tun müsste, und einen Abkündigungspfad für ein Feld vorschlagen, das weg muss. Jedes davon erzeugt einen Entwurf, den das erzeugende Team im Pull Request abnimmt, und genau darum geht es: Die Arbeit, die KI hier leistet, ist das Schreiben, und die Verantwortung bleibt bei dem Team, das die Daten ändern kann.

Wo sie nicht hingehört

Kompatibilität ist ein Vergleich mit einer richtigen Antwort und keine Ermessensfrage: Die Prüfung fragt, ob eine Spalte, die der Consumer liest, noch da ist, noch denselben Typ hat und weiterhin zusagt, nicht null anzukommen. Ein Modell in diesen Pfad zu setzen tauscht ein reproduzierbares Urteil gegen ein plausibles. Kein Modell entscheidet, ob eine Änderung brechend ist, genehmigt eine solche oder schreibt Consumer-Pipelines um, damit sie sie absorbieren — Letzteres ist die zweite Absage oben, automatisiert.

Dieser Abschnitt und Wo KI in der Contract-Arbeit hingehört handeln von KI als Werkzeug in der Contract-Arbeit. KI-Systeme als Consumer der Daten, die ein Contract schützt, sind eine andere Frage und Gegenstand des AI-Readiness-Grundpfeilers.

Was die Runtime durchsetzt, und was Sie selbst laufen lassen können

Beide Hälften sind lauffähiger Code und kein Argument. pipeline-runtime ist öffentlich, lässt sich ohne Cloud-Konto klonen und läuft auf zwei Kommandos. Jede Ausgabe unten ist ein Transkript aus einem echten Lauf, eingecheckt unter docs/evidence/ in diesem Repository, und jedes nennt den Commit, der es erzeugt hat.

Die Consumer-Seite fängt eine Änderung, die bereits ausgeliefert ist. Jeder Deskriptor schreibt die Schemaversion fest, die er liest, und der Executor löst diese Festschreibung gegen den Katalog auf, bevor eine einzige Zeile gelesen wird. Veröffentlicht eine Anwendung eine neue Version einer Tabelle, bleibt die Pipeline stehen, die die alte festgeschrieben hat, und der Fehler nennt die geänderte Spalte und das Release, das sie geändert hat:

fraud_raw.transactions is at v8, descriptor pins v7. v8 was published by
card-ledger release v4.12.0. Diff: + merchant_category_code (VARCHAR,
nullable). Bump the pin to v8 to accept.

Eine Zeile der Programmausgabe, hier für die Spaltenbreite umbrochen.

Das Release steht in dieser Meldung, weil jede Version im Katalog den Producer und die Release-Referenz trägt, die sie veröffentlicht hat. Auf AWS sind das Metadaten, die ein veröffentlichender Job in den Parameters einer Glue-Tabelle setzt, und das haben wir gemessen statt angenommen: UpdateTable schreibt die gesamte Parameters-Map, statt in sie hineinzumischen, sodass jeder Schlüssel, den der Aufruf auslässt, verschwindet und ein veröffentlichender Job Producer und Release bei jeder Schemaänderung mitsenden muss, sonst geht die Zuordnung still verloren. Gemessen wurden alle drei Wege, auf denen sich eine Glue-Tabelle ändert, und zwei davon kosten nichts: Ein Crawler lässt diese Schlüssel unberührt und behält die ersetzte Version, ein gewöhnliches Update ebenso. Die Ausnahme ist Iceberg — die Schlüssel bleiben, aber Glue wird standardmäßig angewiesen, die ersetzte Version nicht zu behalten, sodass auf einer Iceberg-Tabelle eine gepinnte Version verschwinden kann, solange die Engine nicht auf Archivieren konfiguriert ist.

Die Producer-Seite fängt dieselbe Änderung, bevor sie ausgeliefert wird. Das Kommando nimmt zwei Argumente — die Version, die die Anwendung zu veröffentlichen vorhat, und die Pfade, die nach Deskriptoren zu scannen sind — und meldet, welche Pipelines brechen würden, mit einem Exit-Code ungleich null:

$ python -m pipeline_runtime.consumers example/proposals/card-ledger-v5.0.0.yml example/domains
card-ledger release v5.0.0 proposes fraud_raw.transactions v8; the catalog is at v7
2 descriptors under example/domains read fraud_raw.transactions

BREAKS      merchant-settlement-daily (finance)  pins v7  owner [email protected]
            example/domains/finance/pipelines/merchant_settlement.yml
            + merchant_ref (VARCHAR, not null)
            - merchant_id (VARCHAR, not null)   <- breaks

…

2 of 2 consumers fail this check

Das zweite Urteil ist beim … ausgelassen; der vollständige Block steht im Transkript.

Niemand hat einen der beiden Consumer irgendwo registriert: Sie sind die Deskriptoren, die diese Tabelle festschreiben, sie liegen in zwei verschiedenen Domains, und jeder kommt mit der Adresse des Teams, mit dem zu reden ist.

Eine hinzugefügte Spalte lässt diesen Build nicht scheitern, und der Unterschied gehört gesagt. Ein Consumer kann keine Version festschreiben, die es noch nicht gibt, also würde ein Build, der bei jeder Schemaänderung scheitert, jede Schemaänderung blockieren. Die Prüfung scheitert an einer entfernten Spalte, an einer umtypisierten und an einer nicht-null-Spalte, die nullable wird, und meldet den Rest als umzustellende Festschreibungen, mit ihren Ownern.

Diese Regel ist Vorwärtskompatibilität unter anderem Namen — die Eigenschaft, die eine Schema Registry prüft, wenn sie fragt, ob Daten, die unter der neuen Version geschrieben wurden, von einem Consumer auf der alten noch gelesen werden können — und sie unterscheidet sich in zwei Punkten von der Prüfung einer Registry. Verglichen wird gegen die Version, die jeder Consumer tatsächlich festgeschrieben hat, und nicht gegen das zuletzt veröffentlichte Schema, sodass eine Pipeline, die drei Versionen zurückliegt, gegen das geprüft wird, was sie wirklich liest. Und sie ist strenger bei Löschungen, denn eine Registry kann ein optionales Feld verschwinden lassen, wo ein Leser auf dessen Default zurückfällt, während eine SQL-Abfrage, die eine gelöschte Spalte nennt, scheitert, ob diese Spalte nullable war oder nicht.

Ein erfolgreicher Lauf trägt dieselbe Zuordnung nach außen, und das ist der Fall, auf den es am meisten ankommt, denn ein Fehler ist laut, und der stille Durchlauf ist das, wofür ein Contract gekauft wird:

"descriptor_git_commit_signed": true,
…
"source_published_by": "card-ledger",
"source_published_release": "v4.11.0",
"source_schema_version": 7

Vier Felder der Facette; sechs weitere sind beim … ausgelassen.

Die erste Zeile reicht bis zum geprüften Commit, der die Pipeline autorisiert hat; die beiden source_published_*-Felder reichen bis zum Anwendungs-Release, das der Eingabe ihre Form gegeben hat. Ein aus dieser Lineage erzeugtes Verarbeitungsverzeichnis kann damit das Release benennen, das hinter der Zahl steht, die es beschreibt.

Eine Grenze, hier genannt statt dem Finden überlassen: Die Ausgabe einer Pipeline trägt noch keinen Producer, also deckt die Kette die Grenze zwischen einer Anwendung und einer Pipeline ab, nicht die zwischen zwei Pipelines. Die Data Contract CLI liefert seit einiger Zeit eine Kompatibilitätsprüfung auf der Producer-Seite, und sie prüft eine vorgeschlagene Änderung gegen ein Contract-Dokument; der Unterschied hier ist, woher die Liste der Consumer kommt.

Wann dies die falsche Antwort ist

Vom Producer durchgesetzte Contracts sind für mindestens vier Konstellationen das falsche Projekt, und etwas anderes zu behaupten kostet einen Kunden Geld und seine Teams Zeit für einen Mechanismus, der sich nicht auszahlt.

Ein Team besitzt beide Seiten. Anwendung und Pipeline liegen in einem Repository, unter einem Team, und werden zusammen ausgeliefert. Ein Integrationstest in diesem Repository ist der Contract; ein getrennter Mechanismus ist Overhead.

Quellen, die Sie nicht in die CI bekommen. Ein SaaS-System oder eine Hersteller-Datenbank, deren Releases Sie nicht kontrollieren, hat keinen Build von Ihnen, den sie scheitern lassen könnte. Erkennung auf der Consumer-Seite ist dann die primäre Verteidigung und keine Behelfslösung, und dort endet die zweite Absage oben: Sie richtet sich gegen Drift-Behandlung, die als Lösung für Änderungen stromaufwärts verkauft wird, was eine Prüfung auf der Producer-Seite voraussetzt, die es stattdessen hätte geben können — und hier hätte es keine geben können.

Leser außerhalb der Organisation. Beide Hälften des Entwurfs setzen eine Organisation voraus: Die Prüfung liest die Deskriptoren, die die Tabelle festschreiben, und der Lauf löst gegen einen Katalog auf, in den auch die erzeugende Anwendung schreibt, also betreibt jemand beides für beide Seiten. Daten, die über diese Grenze hinaus veröffentlicht werden — an Kunden verkauft, mit einem Partner ausgetauscht oder öffentlich zugänglich gemacht —, lassen die Producer-Hälfte ohne Grundlage, denn diese Leser deklarieren nichts, was der erzeugende Build parsen kann, und manche von ihnen wird er nie kennen. Die Consumer-Hälfte überschreitet die Grenze unbeschadet, und es lohnt zu sagen, welche Hälfte das ist: Ein Team, das fremde Daten liest, kann weiterhin die Version festschreiben, gegen die es geschrieben wurde, und bei einer Abweichung stehen bleiben, und das ist der einzige Schutz für alle, die das Release nicht kontrollieren. Was die Producer-Hälfte ersetzt, ist das, was öffentliche Schnittstellen immer schon genutzt haben: eine veröffentlichte Version mit einem Abkündigungsfenster und einem Datum darin, angekündigt statt abgeleitet, und das Zugriffslog, soweit es die Leser eben erreicht.

Schemata, die konstruktionsbedingt in Bewegung sind. Explorations- und Sandbox-Schichten, wo die brechende Änderung der Zweck ist. Contracts gehören dorthin, wo Daten von etwas konsumiert werden, das Überraschungen nicht verträgt.

Wie Cordata arbeitet

Projekte starten an einem echten Vorfall statt an einem Contract-Katalog. Nehmen Sie eine brechende Änderung aus der Vorfallhistorie des letzten Quartals, verfolgen Sie sie bis zu dem Release, das sie verursacht hat, und verdrahten Sie die Prüfung für diese eine Quelle von Anfang bis Ende — mit dem Anwendungsteam ab der ersten Woche im Raum, wie die Absage oben es verlangt.

Der Zuschnitt ist bewusst schmal: Deklarationen in den konsumierenden Deskriptoren, Release-Referenzen an den Versionen, die der Producer veröffentlicht, die Prüfung laufend in der CI des Producers, und eine Schemaänderung, die als Nachweis durch den neuen Weg geht. Dann die nächste Quelle, wenn sie es wert ist. Die Ergebnisse sind organisatorisch so sehr wie technisch, denn eine Prüfung, der niemand zugestimmt hat, ist eine Prüfung, die jemand abschalten wird.

Übergabe zuerst, wie in beiden veröffentlichten Grundpfeilern: Der Maßstab des Projekts ist, ob die Teams des Kunden das ohne uns auf die nächste Quelle ausweiten.


Wenn Sie ein Schema unter Contract gestellt haben und das erzeugende Team tatsächlich zugestimmt hat — oder zugesehen haben, wie daraus eine Wunschliste in einer Registry wurde, die stromaufwärts niemand geöffnet hat —, würde ich gern hören, welcher Teil gehalten hat und welcher nachgegeben. Die Tür steht offen unter cordata.tech/contact.

Begleitmaterial und weiterführend: cordata-tech/pipeline-runtime ist die lauffähige Fassung von allem oben Zitierten — klonbar ohne Cloud-Konto, mit den Transkripten unter docs/evidence/. Die Pipeline-Hälfte, Teil 1 führt den Deskriptor und die Schema-Festschreibung ein, auf denen dieser Grundpfeiler aufbaut; Teil 2 behandelt die Provenienz, die ein erfolgreicher Lauf nach außen trägt. Der AI-Readiness-Grundpfeiler handelt von dem Consumer, den ein Contract am Ende am meisten schützt: einem Modell, das die Daten weiter unten liest.