Menu

How it works

Most data incidents start upstream: an application team renames a column or changes a status value, and nobody downstream finds out until a dashboard breaks. A data contract turns that implicit dependency into a written interface, like an API spec for data. A typical contract covers:

  • Schema: fields, types, nullability, keys.
  • Semantics: what each field means, units, allowed values.
  • Quality and service levels: uniqueness, valid ranges, freshness and volume expectations.
  • Ownership and versioning: who owns it, how to reach them, and how breaking changes are announced and versioned.

Contracts are written as code (YAML, Avro or Protobuf schemas, or the Open Data Contract Standard) and enforced at several points: in the producer’s CI so a breaking change fails the build, at ingestion where records that break the contract are rejected or quarantined, and in transformation layers. dbt’s model contracts (contract: {enforced: true}) enforce column names and types at build time, and Kafka schema registries enforce schema compatibility for events.

A contract is mostly an organizational tool: the code checks are easy, while agreeing who owns a dataset and who must approve changes is the real work.

Example

A contract for an orders feed and a validator that checks a batch against it. Four violations are reported before the batch can reach consumers (Python 3.11).

# A data contract for the `orders` feed, agreed between the producing and consuming teams.
contract = {
    "owner": "checkout-team",
    "columns": {
        "order_id": {"type": int,   "required": True, "unique": True},
        "status":   {"type": str,   "required": True, "allowed": {"placed", "shipped", "cancelled"}},
        "amount":   {"type": float, "required": True, "min": 0},
    },
}

def validate(rows, contract):
    errors, seen = [], {}
    for i, row in enumerate(rows):
        extra = set(row) - set(contract["columns"])
        if extra:
            errors.append(f"row {i}: unexpected column(s) {sorted(extra)}")
        for col, rule in contract["columns"].items():
            value = row.get(col)
            if value is None:
                if rule["required"]: errors.append(f"row {i}: {col} is missing")
                continue
            if not isinstance(value, rule["type"]):
                errors.append(f"row {i}: {col} should be {rule['type'].__name__}, got {value!r}")
            elif "allowed" in rule and value not in rule["allowed"]:
                errors.append(f"row {i}: {col}={value!r} not in {sorted(rule['allowed'])}")
            elif "min" in rule and value < rule["min"]:
                errors.append(f"row {i}: {col}={value} below {rule['min']}")
            if rule.get("unique") and seen.setdefault((col, value), i) != i:
                errors.append(f"row {i}: duplicate {col}={value}")
    return errors

batch = [
    {"order_id": 1, "status": "placed", "amount": 40.0},
    {"order_id": 2, "status": "SHIPPED", "amount": 25.0},
    {"order_id": 2, "status": "placed", "amount": "10"},
    {"order_id": 3, "status": "placed", "amount": 5.0, "coupon_code": "X1"},
]
problems = validate(batch, contract)
print("contract owner:", contract["owner"], "| violations:", len(problems))
print("\n".join(problems))
contract owner: checkout-team | violations: 4
row 1: status='SHIPPED' not in ['cancelled', 'placed', 'shipped']
row 2: duplicate order_id=2
row 2: amount should be float, got '10'
row 3: unexpected column(s) ['coupon_code']

Common mistakes

  • Writing a contract document that nothing enforces, so it drifts from reality.
  • Putting contracts only on the consumer side, so producers still ship breaking changes.
  • Making every field strict from day one, which blocks harmless additions.
  • Having no versioning or deprecation policy, so the only option for a needed change is breaking consumers.
Search
Filter by type