post data-engineering Β· 2026-06-18 Β· 6 min read
Why data contracts matter more than dashboards
The sentence βthe dashboard looks wrongβ is usually the last symptom of a much older failure.
Somewhere upstream, a field was renamed, an enum gained a new value, a timestamp started arriving in a different timezone, or a producer changed the meaning of a number without changing its type. The pipeline kept running. The warehouse accepted the rows. The dashboard rendered something plausible. Then somebody noticed a chart that did not make sense.
By then, the hard part is not fixing the chart. It is finding out when the meaning changed and who needs to agree on the fix.
This is why I care more about data contracts than dashboards. A dashboard tells you that trust has already been lost. A data contract gives you a chance to keep it.
A schema is necessary, but it is not a contract
Teams often say they have a contract because they have a table definition, an Avro schema, or a Pydantic model. Those are useful, but they only answer part of the question.
A real contract answers four things:
- Shape: what fields exist, their types, and whether they can be null.
- Meaning: what each field represents and which units or conventions it uses.
- Ownership: who can approve a change and who is responsible when it breaks.
- Change rules: which changes are compatible, and how consumers are told about incompatible ones.
Without the last three, you have validation, not a contract.
Consider a field called amount. A schema can say it is a decimal. It cannot tell you whether it is gross or net, which currency it uses, whether refunds are negative, or whether it represents the value at event time or ingestion time. All of those details can change a metric while leaving the schema perfectly valid.
The contract belongs at the boundary
The right place for a contract is where one team or system hands data to another. It does not need to appear at every transformation step in an internal pipeline. Put it at boundaries where assumptions become shared.
Typical boundaries are:
- An application publishing an event for other teams to consume.
- A service exposing a reporting API.
- A pipeline publishing a curated table.
- A third-party feed entering the platform.
- A feature dataset becoming available to another workload.
The producer owns the truth of the data. The consumer owns its use of the data. The contract is the agreement between them.
That sounds obvious, but it changes how you handle changes. A producer should not have to predict every possible downstream use. A consumer should not need access to the producerβs codebase to understand a field. The contract creates a small, explicit surface where those two responsibilities meet.
A small contract is better than a clever one
Do not start by designing a huge specification language. Start with information that is both useful and maintainable.
name: customer_interactionversion: 1owner: customer-platformdescription: One recorded customer interaction with a digital product.compatibility: backward
fields: - name: interaction_id type: string required: true description: Stable identifier for this interaction.
- name: occurred_at type: timestamp required: true description: Time the interaction occurred, expressed in UTC.
- name: channel type: string required: true allowed_values: [web, mobile, api]
- name: duration_seconds type: integer required: false description: Duration of a completed interaction in seconds. constraints: minimum: 0This is intentionally plain. It gives consumers the schema, semantics, ownership, compatibility policy, and constraints without inventing a framework for its own sake.
The exact format does not matter much. JSON Schema, Protobuf, Avro, YAML, a typed Python model, and a warehouse-native constraint can all work. The important part is that the contract is versioned, reviewed, and checked where data enters the shared system.
Compatibility is the part teams forget
Most breakages come from changes that looked harmless to the producer.
Here is a practical compatibility rule set for event-shaped data:
| Change | Usually safe? | Notes |
|---|---|---|
| Add an optional field | Yes | Consumers that do not know it can ignore it. |
| Add a required field | No | Older producers or consumers may not provide it. |
| Rename a field | No | Treat as add + migration + eventual removal. |
| Change a field type | No | Even integer to decimal can break assumptions. |
| Add an enum value | Maybe | Safe only if consumers tolerate unknown values. |
| Make an optional field required | No | Historical data and older producers will violate it. |
| Clarify a field description | Maybe | Safe only if the actual meaning has not changed. |
The awkward one is an enum. Adding partner_api to a channel field sounds backward compatible. But a downstream transformation with a case when channel in ('web', 'mobile', 'api') may send it to an βunknownβ bucket, silently. The schema accepts the event; the business logic does not.
That is why the compatibility policy needs a human review step. Automated checks catch shape changes. They do not reliably catch changed meaning.
Validate at ingestion, not in the dashboard
Validation is most valuable near the boundary where an invalid record arrives.
def validate_interaction(record: dict) -> list[str]: errors = []
if not record.get("interaction_id"): errors.append("interaction_id is required")
if record.get("channel") not in {"web", "mobile", "api"}: errors.append("channel must be web, mobile, or api")
if record.get("duration_seconds", 0) < 0: errors.append("duration_seconds cannot be negative")
return errorsThis is not sophisticated, and it does not need to be. The critical design choice is what happens after validation:
- Valid records proceed normally.
- Invalid records go to a quarantine path with the error reason.
- The producer receives a visible signal.
- The pipeline records the failure rate over time.
Dropping bad records silently is just delayed data loss. Failing an entire high-volume pipeline over one malformed record can be equally unhelpful. A quarantine path gives you a way to preserve evidence, protect downstream consumers, and keep the pipeline moving.
Make ownership visible in the data itself
βTalk to the data teamβ is not ownership. It is a routing failure waiting to happen.
Every shared asset should make its owning team visible in the metadata, and every contract change should have a review path. The reviewer does not need to be a gatekeeper for trivial additions. They need to be accountable for changes that alter the promised meaning or break compatibility.
The useful question during review is not βdoes this compile?β It is:
If this field changes, which people and systems have a reason to care?
That question naturally brings in lineage, downstream consumers, documentation, and release notes. It is less glamorous than a new dashboard, but it is where trust gets built.
Contracts should generate useful work
The best contract is not a document that people admire once. It should drive a few concrete behaviours:
contract definition | +--> producer-side validation +--> consumer test fixtures +--> schema-change checks in CI +--> generated documentation +--> data-quality monitoring +--> catalogue metadataYou do not have to build all of these on day one. Start with validation and change review. Add generated documentation when people begin asking the same field questions repeatedly. Add compatibility checks when shared schemas become a source of incidents.
The point is to avoid parallel sources of truth. If the documentation says a field is optional but the validator rejects nulls, the contract has already split in two.
The migration pattern that saves you later
Breaking changes happen. The answer is not to forbid them forever; it is to make the migration deliberate.
For a renamed field, use a short overlap period:
- Add the new field and keep the old field.
- Publish both for a known period.
- Update consumers and track which ones still read the old field.
- Mark the old field deprecated in the contract.
- Remove it only after the dependency list is empty.
It feels slower than simply renaming a column. It is usually faster than discovering two months later that an obscure weekly report has been calculating zeros since the rename.
What I no longer do
- Use a dashboard as the first data-quality alarm. It is too far downstream and too dependent on somebody noticing.
- Call a schema alone a contract. Types without semantics and ownership do not settle meaningful questions.
- Overwrite contract history. The current definition cannot explain last quarterβs numbers.
- Assume a new enum value is harmless. It often is not harmless to downstream logic.
- Make producers guess every consumerβs needs. Publish a stable contract and give consumers a clear change channel instead.
- Treat documentation as a separate afterthought. The contract should be the source for both validation and explanation.
Closing
Reliable dashboards matter. But by the time a dashboard catches a bad definition, a lot of systems may already have accepted it as truth.
Data contracts move the conversation to the boundary: before a field becomes shared, before a change becomes a silent regression, and before an ownership question turns into a week of archaeology. Keep the contract small, version it, validate it, and give it an owner. The result is not just cleaner data. It is a system where people can trust what a number means.