Beyond Column Types: Building Semantic Data Contracts
By Arshad Ansari
A data contract that lists only column names and types is half a contract. It tells you the shape of the data. It does not tell you what the data means.
Shape checks are easy to automate, so most teams stop there. Then a column called amount arrives on time, as a decimal, never null. Every check passes. The dashboard is still wrong, because amount changed from net to gross last month.
This post is about the other half. I call it a semantic contract: the written meaning that sits beside the schema.
What a type check cannot catch
A type check answers one question: is this value the right kind of thing? It cannot answer these:
- Is this the right thing?
- At the right level of detail?
- In the right unit?
- As of the right time?
- Owned by someone who will tell me before it changes?
Each of these failures produces a valid-looking number. That is why they last so long. Nothing breaks. The number is just quietly wrong, and someone makes a decision with it.
Six things to add to a contract
1. Business definition
Write one sentence per field in the words a business reader would use. "Customer" is the classic case. Sales, support and finance often mean three different things by it. The contract should say which one this field holds, and what it excludes.
If you cannot write the sentence, that is a finding. It means the field has no agreed meaning yet.
2. Grain
Grain is what one row represents. One row per order? Per order line? Per customer per day?
State it as a sentence and name the key that makes a row unique. Most wrong totals I see come from a join that changed the grain without anyone noticing. A contract that states the grain gives you something to test: the key is unique, and the row count behaves as the sentence says.
3. Units and currency
A number without a unit is a guess. Say whether it is dollars or cents, kilograms or grams, a ratio or a percentage. For money, say the currency and whether it is gross or net of tax and fees.
Put the unit in the contract, not only in someone's head. Better, put it in the column name too.
4. Temporal meaning
Time is where the most arguments start. For each date field, say which event it records. Created, shipped, paid and booked are different moments. Say which time zone it uses.
Then say how the data changes over time:
- Is a row final once written, or can it be revised?
- How late can new rows arrive for an old date?
- Is the table a snapshot of now, or a history of every state?
A report that reads "yesterday" from a table that is still being corrected will disagree with itself tomorrow. The contract should say so up front.
5. Ownership
Every contract needs a named owner on the producing side. This is a person or a team, not "data". The owner answers questions about meaning and agrees to tell consumers before changing it.
Without an owner, a contract is a document. With one, it is a working agreement.
6. Compatibility expectations
Say what counts as a breaking change, and what notice consumers get. A useful starting split:
- Safe: adding a new nullable column.
- Breaking: removing a column, renaming it, or changing its type.
- Also breaking, and often missed: changing the meaning, grain, unit or time basis while keeping the name and type.
That last line is the point of the whole exercise. A purely structural contract treats a meaning change as safe, because the schema did not move.
What it looks like
The format matters less than the habit. A YAML file next to the table works. So does a block in the model's documentation. Here is one field:
table: orders
grain: one row per order line
unique_key: [order_id, line_number]
owner: payments-data
fields:
net_amount:
type: decimal
definition: Line total after discounts, before tax and shipping.
unit: USD
time_basis: set when the order is placed; never revised
shipped_at:
type: timestamp
definition: When the carrier first scans the parcel.
timezone: UTC
late_arrival: may be filled in after the order date; null until shipped
compatibility:
breaking: rename, remove, retype, or change of definition, grain, unit or time basis
notice: announced to consumers before release
Nothing here is clever. It is plain text that a person can read and a test can check.
What you can enforce, and what you cannot
Be honest about the limit. Some of this is checkable by machine:
- The unique key is unique.
- Values stay in a sane range for the unit.
- Date fields are not in the future.
- Late arrivals stay within the stated window.
Some is not. A machine cannot tell you that "customer" now means something else. For that, the contract needs a review step: a change to a definition goes through the owner, and the diff is visible in version control.
That split is fine. Automate what you can. Make the rest visible and owned.
Where to start
Do not write a contract for every table. Pick the table behind the number your leadership looks at most. Write the six parts for its ten or so most important fields. You will find at least one field nobody can define. Fix that first.
If you want a quick outside look at where your platform stands, the free Data Platform Teardown is a checklist you can run yourself. It costs nothing and needs no sign-up.
Who this is not for
If you have one analyst and a handful of tables, a full contract process is more weight than you need. A short definitions page and one named owner will do most of the work. Add the rest when a second team starts to depend on your data.
Common questions
- What is a semantic data contract?
- A data contract that states what the data means, not only its shape. It adds the business definition, grain, units, time meaning, owner and change rules to the column names and types.
- Why are column types not enough for a data contract?
- A column can pass every type check and still be wrong. Revenue in the wrong currency, or a count at the wrong grain, is still a valid number.
- Who should own a data contract?
- A named person or team on the producing side. Someone must answer when the meaning of a field changes.
Get new posts by email
Data engineering notes like this one — what breaks and what it costs, in production.
What breaks and what it costs — pipelines, warehouse bills, and the failures that only show up in production. A few a month, never padded to hit a schedule. No sequence, no pitch deck. Reply 'stop' once and you're off — it reaches me, not a queue.
Want the whole playbook?
If this was useful, the long version is my book. Local-First Analytics — 313 pages, runnable code for every chapter — is the full build: DuckDB, Parquet and Arrow, from install to production. On Amazon, and chapter 1 is free to read here.
Get the bookNot ready to buy? Read chapter 1 free — the whole chapter, no email required.
Rather talk it through? Book a free 30-minute call. No slot that suits your time zone? Email info@hikmahtech.in.