Pattern — Schema / Data Contracts
The problem it solves
Two teams (or two codebases, or a vendor and you) agree on the shape and meaning of some data in prose — a design doc, an email, an unwritten "column 15 is income." Nothing enforces it, so the moment one side changes, the data stays type-valid but semantically wrong and every downstream check passes. A vendor inserts a column mid-row and your positional parser reads ZIP codes as income for twelve days (Drill 7). A serving service computes income_to_debt as income / monthly_debt while training used income / (monthly_debt × 12), so the model is fed a feature centered on 48 that it trained on at 4 — for a year (Drill 87). Same shape: the contract lived in two heads, not in code, so drift was silent and discovery came from a human smelling something wrong.
The pattern
Turn the agreement into a machine-checkable data contract — an explicit, versioned spec of the fields, their types, their value ranges/distributions, their semantics, and the delivery cadence — and enforce it at the boundary. Two enforcement points: an ingestion gate that validates every incoming batch against the contract and fail-fast quarantines on any mismatch, and a contract test in CI that fails the build when producer and consumer diverge. Meaning becomes structural: a change that breaks the contract stops the pipeline at your border on day one instead of leaking a confident wrong answer into a decision months later.
How to apply it
- Write the contract as a checked artifact, not prose. A schema file (JSON Schema, Avro, Protobuf,
dbt/Great Expectations suite, or a Pydantic/panderamodel) that pins: column names and types, per-field allowed ranges, null-rate ceilings, an enum of legal categories, and the delivery cadence + a change-notice SLA for external feeds. Version it; a breaking change bumps the major version. - Parse by name, never by position, and fingerprint the input. Reject on unknown, missing, or reordered columns. Hash the header (names + order + types) and diff it against the contract before a single row loads — a mismatch quarantines the whole file (Drill 7's fix).
- Gate on distribution, not just syntax — this is what catches the semantic breaks. Compare each decision-critical field's median and p5/p95 against a trailing 30-day window: warn at a 20% day-over-day shift in the median, hard-stop the pipeline at 50% (median income $60,000 → $10,999 slams it shut). For model features, add a train-vs-serve skew check: page when any top-importance feature's serving mean drifts beyond 2× its training value or its null rate exceeds baseline by 3×, and run a PSI check per served feature — warn at 0.1, page at 0.25 (Drill 87's fix).
- Make parity structural with one definition + a contract test. Compute each feature once — a shared library or feature store consumed by both training and serving — so there is one implementation, not two kept in sync by hand. Back it with a parity contract test in CI: compute the value the producer way and the consumer way on a shared fixture set and assert they match within tolerance; a failing test blocks the deploy.
- Quarantine, don't drop. Rejected batches go to a holding area with an automated diff report, plus a named owner per feed and the vendor's escalation contact in the runbook — so the fix is a review, not an archaeology dig.
flowchart TD
A["Incoming batch / served feature"] --> B["Ingestion gate: parse by name,<br/>fingerprint header vs contract"]
B -->|"schema mismatch"| Q["Quarantine + diff report,<br/>page the feed owner"]
B -->|"schema OK"| C["Distribution gate: median/p5/p95<br/>vs 30-day window, PSI, skew"]
C -->|"median shift over 50% or PSI over 0.25"| Q
C -->|"in range"| D["Load / score on validated data"]
E["Producer and consumer code"] --> F["Parity contract test in CI"]
F -.->|"diverge"| G["Block the deploy"]
When to use it — and when not
Reach for it on every boundary you don't fully control: external vendor feeds, cross-team hand-offs, and the training-vs-serving split — anywhere valid-looking data can be semantically wrong and the cost of a silent error is a bad decision. Skip the heavy machinery where it isn't paid back: low-stakes internal data, throwaway exploratory pipelines, or a single team owning both ends of a transform in one codebase (there the shared function is the contract — a distribution monitor is overkill). The cost is real: contracts must be maintained, thresholds tuned to avoid alert fatigue, and an over-strict gate that quarantines on every benign vendor tweak will get muted — which is worse than no gate. Set ranges from real history, not guesses, and reserve the hard-stop for genuinely decision-breaking shifts.
Signals it's working
- Detection latency collapses from days to seconds: the border alarms on a schema or distribution break the same batch it arrives in, not weeks later via a puzzled analyst.
- Quarantine count is non-zero and acted on — catches are happening and each has an owner and a diff report, not a muted channel.
- CI parity tests fail on real divergence and block deploys; a green build genuinely means producer and consumer agree.
- Track schema-mismatch rate and per-feature PSI / skew as first-class metrics; page on PSI > 0.25 or median shift > 50%, warn at PSI 0.1 / shift 20%.
Seen in the wild
- Great Expectations, dbt tests,
pandera, Pydantic — libraries whose entire job is turning a data contract into a checkable suite at ingestion. - Avro / Protobuf schema registries (Confluent Schema Registry) enforce producer/consumer compatibility on Kafka topics — a schema change that breaks consumers is rejected at publish.
- Feature stores (Feast, Tecton) and Google's Rules of ML prescribe exactly this: one shared feature definition for training and serving, plus logging served features to compare their distribution against training (Rules 29, 31, 37).
- NASA's Mars Climate Orbiter (1999) — pound-seconds consumed as newton-seconds: every number valid, the unit contract unwritten, the spacecraft lost. The canonical semantic-contract failure.
See it
The ChainScene carries the gate — parse by name → fingerprint vs contract → distribution/PSI check → quarantine-or-load — and the CardsScene/TilesScene show the load-bearing numbers (20% warn / 50% hard-stop, PSI 0.1 / 0.25, 2× mean & 3× null skew, 30-day window).
Named concepts: data contracts, schema validation, ingestion gates, contract tests.
