## TL;DR
Write the contract as machine-checkable assertions, schema, nullability, freshness, value ranges, that run on every producer write, with failures blocking the write or paging the producer, never silently reaching the consumer. A contract is only real if breaking it breaks the build. It works because most pipeline breakage is schema or semantic drift, and drift caught at the producer is a failed CI run instead of a 3am page for the consumer team.

```text
data contracts between producers and consumers
```

## Use this when
- A producer change keeps breaking downstream consumers
- You need schema drift caught before it lands, not after
- A new consumer is onboarding to a dataset and needs guarantees
- An agent writes to a table other teams read from

## Not for
- Exploratory datasets with no downstream consumers yet
- A wiki page describing the data, which nobody enforces
- Replacing actual communication about planned breaking changes

## Steps

1. Write the contract as a versioned file next to the producer code:

```yaml
# contracts/orders_v3.yaml
version: 3
dataset: analytics.orders
owner: payments-team
schema:
  - {name: order_id, type: string, nullable: false}
  - {name: amount_cents, type: integer, nullable: false, min: 0}
  - {name: status, type: string, nullable: false, allowed: [paid, refunded, pending]}
freshness:
  max_delay_minutes: 60
sla:
  notify: ["#data-contracts"]
```
Expected output: a file both humans and machines can read, with the owner named so failures have somewhere to go.

2. Implement the assertions as tests that run in producer CI:

```sql
-- contract test: no null order_ids, amounts non-negative, statuses known
SELECT
  COUNT_IF(order_id IS NULL) AS null_ids,
  COUNT_IF(amount_cents < 0) AS negative_amounts,
  COUNT_IF(status NOT IN ('paid','refunded','pending')) AS bad_statuses
FROM staging_orders;
```
Expected output: all zeros. Any nonzero value fails the CI run and blocks the producer change before consumers ever see it.

3. Add a freshness check the orchestrator runs after each load:

```sql
SELECT TIMESTAMPDIFF(MINUTE, MAX(loaded_at), CURRENT_TIMESTAMP()) AS minutes_stale
FROM analytics.orders;
```
Expected output: a number under the contract's max_delay_minutes. Over the limit means the producer is late, and the alert goes to the producer, not the consumer.

4. Version the contract and deprecate with notice:

```yaml
# contracts/orders_v4.yaml
version: 4
supersedes: 3
breaking_changes:
  - "status value 'pending' removed; use 'authorized'"
notice_given_days: 14
```
Expected output: consumers see the breaking change and the notice window in the file itself, so nobody discovers it by debugging a failed dashboard.

5. Add a consumer-side canary that verifies the contract from the other end:

```sql
-- consumer canary: runs before the consumer's own models
SELECT COUNT(*) FROM analytics.orders
WHERE loaded_at > CURRENT_TIMESTAMP() - INTERVAL '2 hours';
```
Expected output: a positive row count. Zero rows means the contract is broken despite green producer CI, and the consumer fails fast instead of publishing stale numbers.

## Variant phrasings

### data contract example
The YAML in step 1 is the whole shape: version, owner, schema assertions, freshness, and who gets paged. Copy it and fill in your columns.

### schema contract enforcement
Schema is the easy half; the contract earns its keep on the semantic half, allowed values, ranges, and freshness, because those are the breaks that schema checks miss.

### producer consumer data agreement
The agreement part is the versioning and notice discipline in step 4. Without it you have tests, not a contract.

## Why it happens
Pipelines break at boundaries: the producer renames a column, adds a status value, or ships late, and every consumer discovers it independently at the worst moment. Contracts move detection left, from consumer dashboards to producer CI, and they assign ownership, so the team that caused the break is the team that gets paged. The versioning matters because data relationships are long-lived; a contract without a deprecation path is just a snapshot of today's assumptions.

## Edge cases
- Emergency break-glass: allow a producer to ship a contract-breaking change with an incident tag, but require the contract file to be updated in the same PR.
- Who owns a failure when the data is technically valid but semantically wrong, like amounts in the wrong currency; put semantic assertions in the contract, not just types.
- Contract sprawl: one contract per dataset, not per consumer, or the producer drowns in slightly different assertions.
- Agents as producers: an agent writing to a contracted table must run the same CI checks; wire the contract tests into the agent's write path.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_AZ9929pmDcdumL3da83FOA
