Dashboards Don't Get Gold Tables

How I am building the gold layer at Pacers Sports & Entertainment: admission tests for business entities, verified grains on a platform that does not enforce keys, and a pressure valve for report-shaped requests.

I am the data architect at Pacers Sports & Entertainment, the organization behind the Indiana Pacers and the Indiana Fever. For the last few months I have been writing down the standards our lakehouse will be held to as we move toward dbt. Most of that writing is internal, but this piece of it is the one I think travels: what earns a table a place in the gold layer, and what keeps it there.

If you have inherited a medallion warehouse before, you probably know the failure mode I am trying to avoid. The gold layer turns into a pile of query results. Tables named after the dashboards that requested them. Column sets that only make sense inside one workbook. Three subtly different versions of “revenue.” Nobody confident about what is safe to drop. The frustrating part is that every one of those tables was reasonable at the moment it was created. A report needed a shape, the shape got materialized, and gold is “the business-facing layer,” so that is where it landed.

The standard I landed on fits in one sentence: the gold layer is small, stable, and reusable. A set of business concepts the whole organization shares, not the residue of every report we have ever shipped. The rest of this post is the machinery I put around that sentence so it actually holds up.

Where gold sits in our stack

Some quick background on the stack so the rest of this makes sense. We run Databricks with Unity Catalog and follow ELT: extract from the source, load it raw, and do all of the transformation inside the platform. If ELT versus ETL or the medallion pattern is new to you, dbt Labs has a solid ETL vs ELT explainer and the Databricks medallion overview covers the layer pattern better than I can in a paragraph. The one-line version is that in ELT, transformation happens after the data has already landed in the warehouse, using the warehouse’s own compute.

Our lower layers are boring on purpose. Bronze is raw: data exactly as the source delivered it, append-only, never edited, so we can always reprocess and audit. Silver is bronze cleaned up: deduplication, type casting, validating and quarantining junk records, and not much else. That is actually stricter than Databricks’ own guidance, which shows joins happening in silver. I keep meaningful transformation out of silver on purpose. A join across sources encodes business meaning, and I want business meaning concentrated in exactly one layer. That layer is what this post is about.

Where the gold layer sits Source systems ticketing APIs CRM · app events Bronze raw, as landed append-only Silver cleanup only: dedupe · cast · validate Gold Entity & fact tables verified grain Conformed dimensions Dashboards & BI Tableau Analysts & ML notebooks · models External syncs CRM · CDP feeds

One row, one business noun

A gold table represents one business entity or event at one declared grain. Every table has to answer “what is one row?” with a business noun: one customer, one ticket transaction at the seat level, one sponsorship contract, one arena entry scan. This is not an idea I invented. dbt Labs calls this the entity layer or concept layer, where each mart represents a single concept at its unique grain, and Databricks describes gold as “semantically meaningful datasets that map to business functions and needs.”

The corollary is the title of this post: dashboards consume gold tables, dashboards do not get gold tables. A dashboard that needs three entities reads three tables, or a thin view over them. When the dashboard is retired, the entities are still valid. If a table’s name or column set only makes sense in the context of one report, it is not a business concept. It is a query result that got materialized.

There is a second rule hiding in here that I am only going to gesture at: gold tables store data, and metric definitions are not data. “Ticket revenue is the sum of net amounts, excluding comps and refunds” is a definition, and a definition should live in exactly one governed place. For us that is a metrics glossary today and a semantic layer eventually. A table is the substrate a metric computes against, never the only place its logic is recorded. The semantic layer needs its own post, so that is where I will leave it.

The admission tests

A table belongs in gold only if all five of these hold:

Test Question to ask
Entity Can you name what one row is using a business noun, without mentioning a report or a tool?
Two consumers Would at least two distinct consumers (teams, dashboards, exports, models, analysts) plausibly use it?
Stability Would the table’s definition survive the retirement of any one dashboard, unchanged?
Business logic Does it encode semantics beyond what silver provides, like joins across sources, derived measures, or business definitions? A rename-and-flatten of one source belongs in silver.
Grain Is there exactly one grain, and can you state it in a single sentence?

All five have to hold, but I do not weigh them equally in review. The two-consumer test is corroborating evidence, not the deciding factor. A new table almost always launches with a single consumer, and blocking a clean entity because only one consumer exists today would be self-defeating. On the other side, a table with five consumers can still be report-shaped. The entity and stability tests do the real work. Two consumers breaks ties.

So what happens when a table fails the first three but somebody genuinely needs the shape? It still gets built. It just gets built as a presentation view (more on those below): cheap, clearly labeled, disposable, and sitting on top of real entities instead of pretending to be one.

The design rules I care most about

The full internal standard has more machinery than a blog post should. These are the four rules I would defend the hardest.

Declare the grain, then verify it, because the platform will not

Kimball’s oldest piece of discipline is to declare the grain before choosing columns. Every gold table’s comment starts with a grain sentence, “one row per entity per qualifier,” and the grain key is declared as the PRIMARY KEY.

Now for the platform reality that shapes the whole rule, and it surprises people coming from traditional warehouses. On Databricks, PRIMARY KEY, FOREIGN KEY, and UNIQUE constraints are informational only. They are never enforced. Only NOT NULL and CHECK constraints actually reject bad rows. A table can carry a perfectly declared primary key and quietly pile up duplicate grain rows underneath it.

Declaring the grain is necessary, but it is not enough. Every gold table also gets an executable check of grain uniqueness that does not depend on the DDL. One of: a dbt unique plus not_null test once the table is dbt-managed, a pipeline expectation that fails or quarantines duplicates, or a scheduled audit query that alerts when COUNT(*) is greater than COUNT(DISTINCT grain_key). Until one of those exists, I do not consider the table conforming, no matter what the catalog says.

To be clear, I still declare the constraints. With the RELY option the optimizer can use them to rewrite queries, and BI tools import primary and foreign key relationships when they build their data models. Informational constraints are useful. They are just not verification.

Wide, but only around the grain

dbt’s guidance for a shop without a semantic layer is to “denormalize heavily.” Pack everything somebody needs about a concept into the concept’s table, because in the modern warehouse storage is cheap and compute is expensive. Benchmarks like Fivetran’s found that a single wide table answered queries faster than the equivalent star schema joins. I would treat the exact numbers as workload-dependent, but the direction is consistent.

Two constraints keep “wide” from sliding back into “dashboard table.” The first is the grain bound: every column added has to describe the table’s own grain. Customer attributes belong on a ticket transaction row, since they describe the buyer of that transaction. A weekly rollup does not. That is a different grain.

The second one took me longer to articulate: attributes and measures are not the same kind of width. Descriptive attributes from other entities (customer name, event date, venue on a transaction row) get denormalized freely. That kind of width is cheap and stays correct no matter what the architecture grows into. Computed measures from other grains (lifetime_value or total_orders sitting on a customer row) get treated with suspicion, because each one bakes a metric definition into a column, and copied definitions are exactly the logic that drifts. I allow them when a consumer genuinely needs the stored value or recomputing is demonstrably too expensive, and in every case the definition also lands in the metrics glossary. The column is a cache of a definition. It is never the definition itself.

Aggregates: allowed, second-class, temporary

The reference material genuinely disagrees here. Databricks happily endorses pre-aggregated gold tables, their example being a weekly sales rollup, while dbt holds that time-based rollups belong past the marts layer, in metrics. My reconciliation for a shop without a semantic layer: entity and fact tables are the system of record for measures, and an aggregate table has to clear a higher bar. The rollup has to be expensive to compute and reused by multiple consumers, with no single-consumer exception. Aggregates are always derived from the entity table, never recomputed from silver, so there is exactly one definition of the underlying numbers. They are named so the rollup is visible (ticket_sales_per_event). And they are a stopgap. An aggregate table is exactly the thing a semantic layer replaces, so every aggregate I do not build now is one I never have to deprecate later.

The materialization ladder, and the rest of the Kimball inheritance

I adopted dbt’s rule of thumb word for word: start every model as a view, promote it to a table when it is too slow to query, promote it to incremental when it is too slow to build. Incremental is complexity you have to earn. It is never the default.

The rest of the dimensional modeling inheritance compresses down to a paragraph, but it matters. Facts that share an entity reference the same conformed dimension, never a private re-derivation inside one pipeline. Fact tables never carry null foreign keys. Unmatched references resolve to a designated unknown-member row, so “unknown” is something you can count instead of something a join silently drops. Around four or five joins in one model is my signal to factor out intermediate models. And every dimension declares its history policy up front: overwrite in place by default, versioned history only when somebody has a demonstrated need for as-was reporting, with surrogate keys and exactly one current row per natural key, covered by the same grain verification as everything else. I will be the first to say none of this is novel. The point is that it is written down and checked instead of assumed.

Not everything business-facing is gold

The layer recognizes five classes of table, and every table declares which one it is. Entity and dimension tables, plus fact and event tables, are the core, and the full standard applies to them. Aggregates are the second-class citizens I just described. The last two classes are the ones I want to spend words on.

Presentation views are report- or tool-specific shapes. They are exempt from the entity, two-consumer, and stability tests, built as thin views over real entities, named for their consumer, and disposable. When the report retires, the view gets dropped, and no deprecation process is owed to anyone.

Integration feeds are outbound contracts whose schema is dictated by, or promised to, an external system: a CRM or CDP sync, a partner export, reverse ETL. Also exempt from the admission tests, also built over entities, but contract-bound. Any schema change goes through the full breaking-change process, and the contract itself (fields, types, semantics, delivery cadence) is documented with the table.

I made these two separate classes because their lifecycles are opposites. A presentation view is the cheapest object in the warehouse to change. An integration feed is the most expensive. Naming that difference is the pressure valve that makes the whole standard livable. Report-shaped and destination-shaped requests are legitimate, they just live on top of gold, so the entity layer stays small and the consumer-specific layer stays either cheap or explicitly governed. (Either class can be materialized when performance demands it. Materialization is an implementation detail. The class is about lifecycle.)

How gold gets consumed Dashboards & BI Tableau workbooks Analysts, DS & ML notebooks · ad hoc · models External systems CRM / CDP sync · exports Presentation views disposable: dropped when the report retires Integration feeds contract-bound: changes go through a deprecation window Gold entity & fact tables one business noun per row · verified grain direct reads

Who actually uses this

Databricks’ medallion guidance lists the gold layer’s intended users as business analysts and BI developers, data scientists and ML engineers, executives, and operational teams. It is the widest audience of any layer. Here is how that maps onto the classes above for us.

Analysts and data scientists read entity and fact tables directly. This is one of the reasons gold is not just a stack of aggregates. Ad hoc analysis and model features want the granular grain, not somebody else’s rollup. BI developers building Tableau workbooks read the same entities, dropping down to a presentation view only when a workbook genuinely needs a bespoke shape. The declared key relationships pay off here too, because the BI tool imports them into its data model. Executives consume the dashboards built on top of all of that, which puts them two steps removed from the tables. Their trust in the numbers depends entirely on grain discipline they will never see. External systems, for us a Salesforce Data Cloud sync, consume integration feeds and only integration feeds. External platforms consume data, not definitions, so feeds always read from gold tables no matter what else the architecture grows.

There is one more consumer category showing up quickly: AI agents. Agents punish ambiguous grains and undocumented semantics harder than any human, because a confused analyst asks a colleague and a confused agent answers confidently. The grain sentences, column comments, and verified keys were worth writing for people. They turn out to be exactly the context an agent needs too.

Keeping the layer honest after day one

The admission tests check stability at birth. Governance is what keeps it true afterward. Every gold table has a named owning team. A breaking change is defined precisely: removing or renaming a column, changing a column’s type or semantics, changing the grain, changing a dimension’s history policy, or redefining a measure’s business logic. Additive nullable columns are not one. Breaking a table means identifying consumers through lineage, announcing a migration path, and defaulting to a thirty-day deprecation window, shipping the new shape alongside the old where feasible. Presentation views are exempt, since they are disposable by definition. A consumer who wants protection from change should be reading the entity table or asking for a feed contract.

Every table also gets re-scored against the admission tests annually, and the outcome vocabulary is deliberately honest: conforms, remediate, demote to view, demote to silver, consolidate, retire, and my favorite, grandfather with sunset date. That last one is for the table that fails the tests but has a consumer hard-wired to its shape. Grandfathered tables are tolerated, not endorsed. They are frozen against new columns and new consumers, with a recorded review date. Every warehouse has those tables. The standard just refuses to pretend otherwise.

What I left out

Metric definitions. Making sure every consumer reporting “ticket revenue” reports the same number is its own standard with its own correctness traps (fan-out joins, additivity, BI tool fine print), and it needs more than a section here. The short version is that a semantic layer sits on top of gold and is only as good as the grain-clean tables underneath it, which is exactly why this post had to come first. That post is coming.

Sources