Most lakehouse problems aren't Spark problems. They're design problems: no agreed layout, every team loading data differently, and nobody sure which table is "the real one". This blueprint gives the decisions to make once, so every pipeline follows the same pattern.
1. The big picture
Sources ──> Landing (files) ──> Bronze ──> Silver ──> Gold ──> Consumers
ADF / Lakeflow raw clean business Power BI, SQL,
Connect / Kafka history conformed products ML, Delta Sharing
| Layer | Purpose | Shape | Who reads it |
|---|---|---|---|
| Landing | Files exactly as received | Files in a volume | Pipelines only |
| Bronze | Raw records + load metadata, full history | Source shape, append-only | Data engineers |
| Silver | Cleaned, typed, de-duplicated, conformed keys | Entity tables (customer, order) | Engineers, data scientists |
| Gold | Business-ready, aggregated or dimensional | Star schema, KPIs, features | Analysts, BI, apps |
2. Unity Catalog layout
Choose one convention and enforce it. Two that work well:
A. Catalog per environment + domain, schema per layer (most common):
dev_sales.bronze test_sales.bronze prod_sales.bronze
dev_sales.silver test_sales.silver prod_sales.silver
dev_sales.gold test_sales.gold prod_sales.gold
B. Catalog per environment + layer, schema per domain: prod_silver.sales, prod_silver.finance.
Pattern A keeps domain ownership clear: one owner group per catalog. Pattern B suits a central platform team.
Whichever you choose:
- Workspace-catalog binding:
prod_*catalogs are only bound to prod workspaces. - Managed location per catalog, each in its own ADLS container, for isolation and cost tracking.
- Owner = a group, for example
sales-data-owners. Pipelines write as a service principal. - Shared reference data (calendar, currency, org hierarchy) goes in a
prod_sharedcatalog.
Naming
| Object | Convention | Example |
|---|---|---|
| Bronze table | source_entity | sap_vbak, crm_contacts |
| Silver table | business entity, singular or plural (pick one) | customers, orders |
| Gold dimension / fact | dim_ / fact_ | dim_customer, fact_sales |
| Gold aggregate | agg_ + grain | agg_sales_daily |
| Technical columns | _ prefix | _ingest_ts, _source_file, _batch_id |
3. Table standards
Apply them with a template or in a shared library (see the Python wheel post):
CREATE TABLE prod_sales.silver.orders (
order_id STRING NOT NULL,
customer_id STRING,
order_ts TIMESTAMP,
amount DECIMAL(18,2),
status STRING,
_ingest_ts TIMESTAMP,
_batch_id STRING,
CONSTRAINT orders_pk PRIMARY KEY (order_id) -- informational, helps BI and the optimizer
)
CLUSTER BY (order_ts, customer_id) -- liquid clustering on common filters
COMMENT 'One row per sales order; latest state. Source: SAP VBAK/VBAP.'
TBLPROPERTIES ('delta.enableChangeDataFeed' = 'true');
ALTER TABLE prod_sales.silver.orders ADD CONSTRAINT valid_amount CHECK (amount >= 0);
- Managed tables by default. Enable predictive optimization so OPTIMIZE and VACUUM run automatically.
- Liquid clustering instead of partitioning for most tables. Partition only very large tables with a natural, low-cardinality key such as date, and only if you need it.
- Comments on tables and key columns. They feed search, AI/BI Genie and documentation.
- Change Data Feed on silver tables that feed incremental gold.
- Tags for classification (
pii,confidential) to drive masking and policies.
4. Standard loading patterns
Give every pipeline one of these five patterns. Don't invent new ones.
| Pattern | Use for | How |
|---|---|---|
| Append | Events, logs, bronze | Auto Loader / streaming append; de-duplicate on a key within a watermark |
| Upsert (SCD1) | Latest state of an entity | MERGE on business key, newest record wins (sequence column) |
| SCD Type 2 | History of attribute changes | AUTO CDC ... STORED AS SCD TYPE 2 in a Lakeflow pipeline, or MERGE with valid_from / valid_to / is_current |
| Snapshot replace | Small reference tables, full extracts | CREATE OR REPLACE TABLE ... AS SELECT, or INSERT OVERWRITE |
| Partition replace | Recomputing a time window in gold | INSERT INTO ... REPLACE WHERE order_date >= '2026-10-01' |
-- partition replace: recompute the last 3 days idempotently
INSERT INTO prod_sales.gold.agg_sales_daily
REPLACE WHERE order_date >= current_date() - 3
SELECT to_date(order_ts) AS order_date, region, count(*) AS orders, sum(amount) AS revenue
FROM prod_sales.silver.orders o JOIN prod_sales.silver.customers c USING (customer_id)
WHERE to_date(order_ts) >= current_date() - 3
GROUP BY ALL;
Every load must be idempotent. Rerunning the same batch gives the same result. Then retries and backfills are safe.
5. Data quality
| Layer | Checks | On failure |
|---|---|---|
| Bronze | File arrived, schema readable, row count > 0 | Alert; keep loading (rescue bad records) |
| Silver | Not-null keys, valid types and ranges, referential checks, duplicates | Quarantine bad rows; fail if above a threshold |
| Gold | Reconciliation to silver/source totals, freshness | Fail the publish; keep the last good version |
In Lakeflow declarative pipelines, use expectations:
CONSTRAINT valid_key EXPECT (order_id IS NOT NULL) ON VIOLATION DROP ROW,
CONSTRAINT valid_amount EXPECT (amount >= 0) ON VIOLATION FAIL UPDATE
For continuous checks such as freshness and anomalies, add data quality monitoring (Lakehouse Monitoring) on key gold tables.
6. Compute per workload
| Workload | Compute |
|---|---|
| Scheduled pipelines | Lakeflow jobs on job compute (policies) or serverless jobs |
| Declarative pipelines | Serverless pipelines where allowed |
| BI and SQL | Serverless SQL warehouses, separate for BI vs ad hoc |
| Data science | Dedicated (single-user) clusters, ML runtime |
| Development | Small personal compute with auto-termination |
7. Gold as data products
A gold table is a product with an owner and a contract, not just a table.
- Owner group and support channel in the table comment or tags.
- Documented grain, keys and refresh schedule (SLA).
- Stable schema. Breaking changes go to a new table or version (
fact_sales_v2) with a deprecation period. - Access granted to consumer groups; row filters and masks where needed.
- Shared outside the platform with Delta Sharing, not file exports.
- Certified with tags (for example
certified=true), so users know which tables to trust.
8. Operations
- Lineage comes automatically in Unity Catalog. Use it for impact analysis before changing silver tables.
- System tables for cost per catalog and job, and for failed runs (see the monitoring post).
- Time travel retention of at least 7 days on silver and gold; deep clone backups for critical tables.
- CI/CD with Asset Bundles: dev, then test, then prod, with the same code and different catalog names passed as variables.
# bundle variable makes the same pipeline target dev_sales / prod_sales
variables:
catalog:
default: dev_sales
targets:
prod:
variables:
catalog: prod_sales
Anti-patterns
| Anti-pattern | Why it hurts |
|---|---|
| BI reading bronze or silver directly | Every report re-implements business logic differently |
| One huge "data lake" catalog with personal schemas | No ownership, no isolation, permission sprawl |
| Over-partitioning (by ID or hour) | Millions of tiny files; slow reads |
| Non-idempotent appends in silver/gold | Duplicates on every retry |
| Hard-coded catalog names in code | Can't promote between environments |
| Gold tables without owners | Nobody fixes them when they break |
Tip
Write these decisions down as a one-page lakehouse standard in your repository: layout, naming, the five loading patterns and the data-product checklist. Review new pipelines against it in pull requests. Consistency matters more than any single choice.