BitBootcampBitBootcamp ← all explainers 🔄 Review ⚙ Settings

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

  1. Write the contract as a checked artifact, not prose. A schema file (JSON Schema, Avro, Protobuf, dbt/Great Expectations suite, or a Pydantic/pandera model) 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.
  2. 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).
  3. 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).
  4. 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.
  5. 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

Seen in the wild

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.