How do you enforce a data contract in CI on the producing team's pull requests?
answer
- the check belongs where the change is made
- producer's repo, not the pipeline's
- diff proposed spec against the published one
- compatible passes, breaking blocks the PR
- also prove the code matches the spec
basics
~20 sKeep the contract spec in the producer's repository and add a CI job that diffs the proposed spec against the released one, classifies each change as compatible or breaking, and fails the pull request on a breaking change unless a major version bump and consumer sign-off accompany it.
solid answer
~50 sEnforcement has to sit where the change is made, so the contract file lives in the producing service's repo and its CI does three things. First, **diff the spec**: compare the proposed version against the last published one and classify each delta — added optional field is compatible, removed field, narrowed type, changed units or grain is breaking. Second, **prove the code matches the spec**: generate or validate the emitted payload or table DDL against the contract, so a rename in the code that nobody reflected in the YAML fails too. Third, **gate**: a breaking delta fails the build unless the PR carries a major version bump and the required approvals. On merge, publish the new version to a catalog or registry so consumers can see it. Everything checked after the data lands is detection, not enforcement — the rows already exist by then.
code
text · 8 linescontract-check: orders.order_placed (published 2.1.0)
+ field channel added, optional COMPATIBLE
~ field gross_amount_cents integer -> string BREAKING
- field coupon_code removed BREAKING
FAIL: 2 breaking changes on a minor version bump.
Required: version 3.0.0 + approval from group data-consumers,
or mark coupon_code deprecated instead of removing it.go deeper
Know that a data contract can be checked automatically, and that the check runs in the pull request of the team producing the data rather than after the rows have already loaded.
Explain the three parts — diff the proposed spec against the published one, prove the emitted structure matches the spec, then block on breaking deltas — and be able to classify example changes as compatible or breaking.
Show the escape hatch and its price: how a genuinely breaking change gets through with a major version, named approvers and a deprecation window, and why a freely available skip label destroys the whole mechanism.
Own the rollout economics: bootstrapping contracts from what producers already emit, running warn-only before blocking, and deciding which datasets are worth a gate at all given the review load it puts on producing teams.
## Why the check belongs in the producer's pipeline A data contract that is only validated downstream tells you a pipeline is already broken. The rows have landed, the dashboard is already wrong, and the fix requires a backfill plus a conversation. The whole point of a contract is to move that conversation earlier — to the pull request where the producing engineer typed the rename. That means the enforcement artifact must live in the producing repository and run in the producing team's CI, alongside their unit tests, with the same blocking semantics. This is also what makes contracts socially viable. Producers accept a check that fails their build with a clear message far more readily than a policy owned by another team that emails them after an incident. ## Check one: diff the spec against the last published version The core job compares the contract file on the branch against the version currently published, and classifies each delta: - **Compatible**: adding an optional field, widening a type, relaxing a constraint, adding an allowed enum value that consumers treat as unknown, improving a description. - **Breaking**: removing a field, renaming a field, making an optional field required, narrowing a type, changing units or timezone, changing the key or the grain, removing an allowed value. The classification list must be written down and agreed, because the arguments happen at the edges: is adding an enum value breaking? It is, for a consumer with an exhaustive `CASE`. Decide once, encode it in the checker, and stop relitigating it per PR. ## Check two: the code actually matches the spec A spec diff alone is game-able — an engineer renames the column in the DDL and simply does not touch the YAML, and CI is happy. Close that hole by making the spec load-bearing. Either **generate** the emitted structure from the contract (target DDL, the event's serialization schema, a typed producer class), or **validate** a real sample against it in a test: serialize a fixture event, assert every contract field is present with the declared type and nullability, assert no undeclared required field appeared. Generation is stronger because drift becomes impossible rather than merely detected. ## Check three: the gate On a compatible delta, CI passes and the contract's minor or patch version increments. On a breaking delta, CI fails with a message naming each breaking change and what the version would have to become. The escape hatch is deliberate and expensive: bump the major version, obtain the approvals your change policy requires (often a named consumer group as a required reviewer on the contract path), and typically pair it with a deprecation window rather than an immediate removal. The cost of the escape hatch is the entire mechanism — if any engineer can add a skip label, the gate has no teeth. ## Publication on merge When the PR merges, CI publishes the new contract version to wherever consumers look — a catalog, a registry, a versioned artifact repository — and ideally announces it on the channel the change policy names. Consumers then have a machine-readable record of what changed and when, which is what lets a downstream team pin, test against, or migrate to a version deliberately. ## What CI cannot enforce Be honest about the boundary in an interview. CI checks the *declared* interface. It cannot know that the producer changed the business rule behind a field so `status = 'complete'` now includes partially-refunded orders while the type and name stayed identical. Semantic drift of that kind is caught by contract-attached expectations that run on real data — volume, distribution, freshness — and by the description review in the PR. Nor can CI enforce anything about a dataset produced by a system you do not control: for a vendor SaaS or a third-party database there is no PR to gate, and the contract degenerates into a monitor at the boundary. ## Rollout in an existing codebase Introducing this on a repo with dozens of tables all at once will fail. The workable path is to bootstrap a contract from what the producer already emits, so the first version is definitionally green, then turn the check on in warn mode until the noise is gone, then make it blocking for that one dataset. Only after the first breaking change is caught in a PR — visibly, cheaply — is there an argument for expanding coverage.
- An engineer renames a column in the DDL but does not touch the contract file. Does your CI catch it?Only if the spec is load-bearing. A spec-versus-spec diff sees nothing, so you must also assert the emitted structure against the contract — validate a serialized fixture or generate the DDL from the spec. Generation is stronger: drift becomes impossible rather than merely detected after someone forgets.
- Is adding a new allowed value to an enumerated field a compatible or a breaking change?It depends on the consumer contract you have chosen, so decide once and encode it. A consumer with an exhaustive CASE or a non-nullable join to a lookup table breaks; a consumer that maps unknown values to 'other' does not. Most teams classify it as breaking unless the contract explicitly requires consumers to tolerate unknown values.
- What kind of contract violation can CI never catch?Semantic drift with no structural signal. If the producer changes the rule so status = 'complete' now includes partially-refunded orders, the name, type and nullability are all unchanged and every static check passes. That class is caught only by data-level checks attached to the contract — volume, distribution, reconciliation — and by reviewing the field description in the PR.
saying these in an interview costs you the question
- Running the contract check only in the data pipeline after loading
- Letting a skip label bypass the breaking-change gate routinely
- Diffing only spec-to-spec so code drift goes undetected
- Never defining what counts as breaking, arguing it per PR
- Claiming CI can catch a changed business rule behind a field