Data Engineering glossary
What is a Data Contract?
A data contract is an explicit, versioned agreement between the team that produces a dataset and the teams that consume it. It defines the schema, the meaning of each field, quality rules, freshness and the owner, and it is checked automatically, so a breaking change is caught before release instead of breaking downstream pipelines.
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.

