posts.

Gold Is a Location, Not a Guarantee

The medallion architecture divides a lakehouse into three layers, and anyone can read them without instruction: bronze is what arrived, silver is what’s been cleaned, gold is what you can use.

Then two teams query the same gold table and report different results. Or someone discovers midway through an outage that nobody knows who approves changes there.

A layer tells you where the data lives. It never tells you what it promises.

What the documentation promises, and what it doesn’t

Read the source before you argue with it. Databricks' documentation is explicit about the medallion pattern’s status: adopting it is “a recommended best practice but not a requirement”. It’s a naming convention, not a mechanism.

On guarantees, that same page states that “this architecture guarantees atomicity, consistency, isolation, and durability”. That deserves a pause.

ACID doesn’t come from the three-layer arrangement. It comes from the table format underneath, which is a separate, swappable piece. Three folder names do not buy you atomicity, isolation, or durability. The documentation attributes a property to the architecture that actually comes from something else — and never says what.

It’s the same displacement the rest of this post will describe, and it appears in Databricks’ own documentation. Even granting it entirely, ACID says nothing about whether the number is right, whether the grain matches your assumption, or whether anyone is accountable.

Validation does appear, but split across layers: bronze has minimal checks, and silver is where schema enforcement, deduplication, and quality tests land. Gold, according to that page, surfaces aggregated data for downstream consumers. Aggregating is not validating.

Trust gets inferred from the path

The problem isn’t the layers. It’s what the name does to the person who reads it.

flowchart LR
    Q{"Can I trust<br/>this GOLD table?"} --> A["It sits in GOLD"]
    A --> U["Which checks ran?<br/>Who owns it?<br/>What grain?"]
    U --> N["Unstated"]
theory of small decisions

Nobody argues this reasoning aloud. It just happens because a word like gold does rhetorical work that nobody asked it to do. Ananth Packkildurai, who wrote about data platforms in Data Engineering Weekly, says it plainly: “data architecture is not a medal competition.”

Because trust gets inferred rather than declared, there’s nothing that fails when it stops being true. A gold table that no longer holds what someone assumed remains in gold.

A guarantee is declared, not located

The alternative isn’t another layer. It’s writing what the table promises somewhere a machine can read it.

That’s a data contract. Bitol, the incubating standard at the Linux Foundation, defines it as a commitment between the data’s producer and its consumers, stating the quality rules it must meet, service levels, roles, and ownership. The difference from a layer is that here there’s a subject: someone stands behind something specific.

The same shift exists at the assertion level. Great Expectations formalizes expectations as verifiable claims about a dataset — literally “unit tests for your data”. An expectation either passes or fails, which is exactly what a folder cannot do.

A contract declares, and its enforcement depends on the tools that run it. The YAML file isn’t the guarantee.

But a written, verifiable promise is a different category from a promise inferred from a path.

That distinction is the entire argument. It echoes the same tension I laid out in the ontology of the semantic layer: what’s visible is not what bears the weight.

Gunnar Morling, who led Debezium at Red Hat, develops the same idea from the producer’s side.

The producer doesn’t expose its internal schema in the hope nobody depends on it. Instead, it decides what it promises and holds that promise steady even as it changes underneath.

Promote, don’t deposit

There’s a pattern that makes that promise part of the pipeline. The Apache Iceberg documentation describes write-audit-publish: writes land on an audit branch isolated from the main history, a validation flow examines that state, and only then is it promoted.

flowchart LR
    W["Write to audit branch"] --> V{"Run declared<br/>expectations"}
    V -- pass --> P["Publish to main"]
    V -- fail --> D["Discard branch"]
    P --> R["Consumer reads<br/>a checked promise"]
theory of small decisions

The verb is what changes. Data doesn’t get deposited into a trustworthy location; it earns the right to land there. The guarantee is the validation step, not the destination.

Failure modes worth naming

SymptomUsually means
Two teams query gold and report different numbersThe layer doesn’t declare grain or filters; each team assumes its own
Nobody knows who approves changes to a gold tableThere’s a location, not an owner
The consumer catches the failure before the producerNo expectations verified before promotion
The postmortem says “it was in gold, should have been fine”Trust inferred from location
Someone proposes adding a platinum layerYou’re seeking the answer in the layers when it only lives in the promise
The table has a contract and still surprises whoever uses itIt declares expectations nobody runs
Correct data arrives late and nobody noticesThe layer talks about quality, not freshness or SLA

The pattern holds across all seven: someone trusts a promise that nobody is checking.

Where to start

Don’t migrate anything. Take the gold table that the most people query and write three sentences in a file next to it: what one row means, what it guarantees about freshness, and who answers when it stops being true.

Then convert one of those three into a check that runs with every load. Which one you choose matters less than the fact that it can fail on its own. Until something fails, you still have a well-named location, not a promise.

Lakehouse - Data Contracts - Data Quality - Apache Iceberg

The Chat Always Answers, and That's the Problem

A chat on top of your warehouse feels like you’ve finally crossed the finish line. Users ask in plain language, the model generates SQL, and the endless queue to your data team simply disappears.

Then someone asks how many active customers you had last quarter. They get a number back. They carry it into a meeting. Nobody can say what it counted as a customer, or as active, or whether it left out test accounts.

The chatbot does not fail at language. It fails at meaning.

The failure doesn’t look like an error, it looks like a number

A system that doesn’t understand a question has two moves: refuse, or invent. A language model almost always invents, and it does so with flawless prose.

dbt Labs frames it this way: with text-to-SQL, failure looks like a plausible but wrong answer. A declared layer fails by returning an error message instead.

That distinction is real. Worth knowing it comes from dbt Labs, though—they sell the declared layer, and the page that makes this distinction is their own benchmark running itself against text-to-SQL. I’ll return to this.

Still, the distinction holds even if you trust zero numbers from anyone. An error stops someone and gets fixed. A plausible number slides into a deck and spreads.

Asking is not agreeing

Natural language questions do not make modeling decisions disappear. They push those decisions into a place where nobody sees them happen.

flowchart TD
    Q["How many active customers<br/>last quarter?"] --> M{"Is 'active customer'<br/>declared anywhere?"}
    M -- yes --> S["Resolved against entity,<br/>grain and filters"]
    M -- no --> G["Inferred from column names<br/>and a best guess"]
    S --> A["One answer, or an error"]
    G --> P["A plausible number"]
theory of small decisions

The left branch only exists if someone already wrote what an active customer is. If nobody did, the model does not pause to ask: it makes the choice itself, and that choice is not recorded anywhere.

This is the same problem I described in the ontology of the semantic layer, but with a difficult difference. In that post, disagreement eventually surfaced because two teams reported different revenue numbers.

Here each person asks alone, in their own chat window, and no one compares.

What the benchmarks measure, and what they don’t

Worth examining numbers that were measured with care, and knowing what they do and don’t show.

The BIRD benchmark, presented at NeurIPS 2023 and built across large, messy databases, showed ChatGPT achieved 40.08% execution accuracy compared to 92.96% for annotators. Before that, Spider identified the real hard part: not writing SQL, but generalizing to schemas the model has never encountered.

Today’s models score better than those. But here is the subtle point that matters: these benchmarks do not measure what this post is about. They compare the generated query against gold-standard SQL written by an annotator, which means someone already resolved what the question meant.

The ambiguity in “active customer”—that is exactly what the measurement leaves out. The lab tests whether the model translates a settled intention correctly. Inside your company, nobody settled the intention yet.

An agent cannot reason about what nobody wrote

The answer is not to ban the chat. It is to write down the definition somewhere, and make the agent read that file.

# The question from the beginning, resolved against something written.
metric:
  name: active_customers
  entity: customer
  grain: "one row per account, not per contract"
  filters:
    - "status = 'active'"
    - "is_test = false"
  time_dimension: subscription_period

Ten lines that no chat can reinterpret three different ways. With this in place, the question from the first paragraph has exactly one answer.

flowchart LR
    Q["active customers<br/>last quarter"] --> E["entity: customer"]
    E --> G["grain: one per account"]
    G --> F["filters: active,<br/>not test"]
    F --> T["time: subscription_period"]
    T --> A["One resolved query"]
theory of small decisions

Every step in that chain is a choice somebody made once. Without the file, the model makes all five choices itself, and can make them differently each time.

Worth saying where this half comes from. Cube and Malloy document this approach, and both sell products that implement it. Open Semantic Interchange is a vendor-neutral standard, though “neutral” here means neutral among vendors: it is backed by Snowflake, dbt Labs, Databricks, and Salesforce.

Four of this post’s seven sources sell the solution it describes. I do not know of any peer-reviewed independent measurement of how much a declared layer improves accuracy— including the dbt benchmark I cited. Judge the argument by how it works, not by the numbers behind it.

Lloyd Tabb, who created LookML and Looker before Malloy, makes his case in a research talk, not a product pitch.

He argues that SQL forces you to rebuild meaning from scratch every time, and writing it once changes who has to understand it.

Failure modes worth naming

SymptomUsually means
Two people ask the same question and get different numbersEach query rebuilt the definition, and nobody checked them against each other
The chat never refuses an answerNothing is declared for it to fail against
The answer shifts when you ask the same thing differentlyThe model is guessing the filter from your wording
Nobody recalls what “customer” meant in that number now circulatingThe definition was decided in a chat, not in the model
Your data team reviews outputs instead of definitionsYou inspect the answer because the input was never written down
It works in the demo and fails on real dataThe demo tables had no ambiguous columns to resolve

The pattern across all six: the model was told to choose something the team never agreed on.

Where to start

Do not enable chat yet. Find your three most-asked questions in Slack and write out each one: which entity answers it, what is the grain, what are the filters.

If writing them down shows disagreement between team members, you have just discovered what the chat would have answered too—without warning anyone.

References

  1. dbt Labs — Semantic Layer vs. Text-to-SQL: 2026 Benchmark Update, dbt Developer Blog
  2. Cube Dev — Introduction, Cube Documentation
  3. Jinyang Li et al. — Can LLM Already Serve as A Database Interface? A BIg Bench for Large-Scale Database Grounded Text-to-SQLs, NeurIPS 2023
  4. Tao Yu et al. — Spider: A Large-Scale Human-Labeled Dataset for Complex and Cross-Domain Semantic Parsing and Text-to-SQL Task, EMNLP 2018
  5. Malloy Data — Querying a Semantic Model, Malloy Documentation
  6. Open Semantic Interchange — Introduction, OSI specification
  7. Lloyd Tabb — Malloy: A Modern Open Source Language for Analyzing, Transforming, and Modeling Data, CMU Database Group, 2025
Semantic Layer - Text-to-SQL - Data Product - LLM

C4 for Data Architecture: The Wrong Container

Adopting C4 is sold as the end of diagram chaos. Four levels, one audience each, and everybody finally draws the same thing.

On a data platform it half works. The diagram gets drawn, the review approves it, and six months later nobody opens it to decide anything.

The level of detail was never the problem.

Every data platform diagram looks the same

Draw the container level for three platforms at three different companies and you get three versions of one picture: an ingestion layer, object storage, a processing engine, an analytical warehouse, an orchestrator and a BI tool.

flowchart LR
    F["Source systems"] --> I["Ingestion"]
    I --> L["Object storage"]
    L --> P["Processing engine"]
    P --> W["Analytical warehouse"]
    W --> B["BI and consumption"]
    O["Orchestrator"] -.-> I
    O -.-> P
theory of small decisions

The picture is accurate and says nothing. Two platforms with that exact shape can be opposites in the only things that matter: who owns each dataset, what grain it lives at, what is promised about it, and what breaks when someone changes it.

A diagram that cannot tell two architectures apart is not describing either of them.

What C4 does solve, and it is not the boxes

It is worth separating what C4 contributes from what gets attributed to it. Its real contribution is scope: one level of abstraction and one audience per diagram, which kills the forty-box picture where a team, an endpoint and a table all sit together.

It is also notation independent and tooling independent, which matters more than it sounds. Arguing about what shape each box should be consumes more time than anyone admits.

The talk keeps returning to something that gets lost on adoption: the levels are maps of one territory at different scales. If the territory you care about is not the processes that run, zooming in will not fill the map.

A C4 container is a place, not a dataset

The definition is explicit: a container is an application or a data store, something that has to be running for the system to work. A database counts. A file system counts.

That is a runtime abstraction, which is why the template fills up with Kafka, Spark and the warehouse. But the decisions that define a data architecture do not live at runtime: entity identity, table grain and dataset contracts do not need to be running to exist, and they are what quietly holds up every metric.

C4 has no box for that. It is not a flaw in the model — it was designed for systems where behaviour lives in the code and data is a persistence detail. On a data platform that relationship is inverted.

Making the dataset the container, and what it costs

The alternative is a deliberate substitution: the container becomes the data product, the components are the models that build it, and the runtime drops down to the deployment diagram, which is where C4 expected it anyway.

flowchart TB
    subgraph D1["Domain: Sales"]
        PD1["Data product: Orders<br/>grain: one row per order"]
        M1["model: orders_clean"] --> PD1
        M2["model: orders_enriched"] --> PD1
    end
    subgraph D2["Domain: Finance"]
        PD2["Data product: Revenue<br/>grain: one row per line"]
    end
    PD1 -->|"contract v2<br/>daily SLA"| PD2
    PD2 --> C["Consumers"]
theory of small decisions

This diagram does change when the architecture changes, because the arrows are promises between teams rather than network paths. And it has a real cost: operations disappear from view. Nobody looks at this picture to understand why last night’s job failed, and anyone who tries will feel the diagram is lying to them.

It earns its place while the incoming question is a design question. When the question comes from on-call, a different diagram is the one that helps.

Level 4 is generated, not drawn

C4 already says the code level is optional and should come from tooling rather than from anyone’s hand. In data that guidance is stronger still, because level 4 is lineage, and lineage changes every week.

A dbt project already produces it: dbt docs generate compiles the project and exports the full DAG straight from the repository. A hand-drawn lineage diagram is wrong within the month it was drawn, and worse than having none, because someone will believe it.

Failure modes worth naming

SymptomUsually means
The diagram would fit any company in the sectorThe runtime got modelled, not the architecture
Every team keeps its own copy of the same pictureNobody agreed what a container means here
The diagram is only updated before an auditIt is used to justify, not to decide
Drawn lineage disagrees with the warehouseLevel 4 drawn by hand instead of generated
“Where does this table go?” asked at every reviewNo domain level between system and model
Nobody knows who to ask for a changeThe boxes carry technology, not ownership

The pattern in all six: the diagram describes where data runs instead of what it promises.

Where to start

Not by adopting C4 wholesale. Take a single domain, draw its container level twice — once with the technologies and once with the data products — and show both to someone who does not work in it.

The second one will prompt questions and the first one will not. That difference is the whole argument, and finding it costs an afternoon instead of a quarter of standardisation.

C4 - Data Modeling - dbt - Data Contracts