The medallion pattern is useful when each layer has a different contract. It is not useful when a diagram simply needs three colors.
- Bronze preserves source fidelity and history.
- Silver validates, deduplicates, harmonizes, and enriches detailed data.
- Gold serves a defined business use, such as reporting, an application, or machine learning.
This walkthrough uses a Fabric trial workspace and a small orders dataset. The
Files/raw folder is a landing and replay boundary, not a fourth quality tier.
Bronze is the durable Delta representation of that input.
The contracts
| Layer | Main question | Typical work | Consumers |
|---|---|---|---|
| Landing / ingestion | Can I replay what arrived? | Preserve files, source metadata, arrival time, batch ID | Ingestion and operations |
| Bronze | What did the source tell us? | Append source-faithful records with minimal handling | Engineering, audit, replay |
| Silver | What do we believe is valid? | Validate, deduplicate, harmonize, join, quarantine | Engineers, analysts, data scientists |
| Gold | How should the business consume it? | Models, aggregates, KPIs, serving optimization | Power BI, applications, business teams |
Every boundary should clarify ownership, quality, recovery, access, or performance. Otherwise the layer is architecture theatre.
What we will build
landing/raw_orders.csv
|
v
bronze_orders
|
v
silver_orders
|
v
gold_orders_by_region
Prerequisites
You need:
- a Fabric workspace;
- a Lakehouse;
- a raw CSV file; and
- a notebook with PySpark.
1. Create the workspace and Lakehouse
Create:
blog_lakehouse_medallion
sales_lakehouse
For this walkthrough, create:
Files/raw/
Managed Delta tables live under Tables; matching Files/bronze,
Files/silver, and Files/gold folders are not required. Larger designs may
use separate schemas, Lakehouses, or workspaces when ownership or security
requires it.
2. Load the raw data
Upload:
order_id,customer_id,order_date,amount,status,region
1001,501,2026-09-14,124.90,paid,North
1002,502,2026-09-15,80.00,paid,South
1003,503,2026-09-16,210.75,fulfilled,North
1004,504,2026-09-17,52.10,returned,West
Store it at:
Files/raw/orders.csv
Read it:
from pyspark.sql import functions as F
raw_df = spark.read.option("header", True).option("inferSchema", True).csv("Files/raw/orders.csv")
raw_df.show(10)
Keep this landing copy unchanged so transformation logic can be replayed.
3. Create Bronze
The compact tutorial applies technical normalization:
bronze_df = (
raw_df
.withColumnRenamed("order_id", "order_id")
.withColumnRenamed("customer_id", "customer_id")
.withColumn("order_date", F.to_date(F.col("order_date")))
.withColumn("status", F.lower(F.trim(F.col("status"))))
.withColumn("amount", F.col("amount").cast("decimal(18,2)"))
.na.fill({
"region": "Unknown",
"status": "unknown"
})
)
bronze_df.write.mode("overwrite").format("delta").saveAsTable("bronze_orders")
For stricter source fidelity, append instead of overwrite and add source file, ingestion timestamp, batch ID, and source-system metadata. Move value cleanup into Silver.
4. Create Silver
Silver should retain the lowest useful business grain. In this example it keeps one detailed row per order and applies a small contract:
- records without an amount are excluded from the accepted table;
- dates and amounts have stable types;
- derived columns have documented meaning; and
- the order grain remains intact.
silver_df = (
spark.table("bronze_orders")
.filter(F.col("amount").isNotNull())
.withColumn("net_amount", F.col("amount"))
.withColumn("order_month", F.date_format(F.col("order_date"), "yyyy-MM"))
.withColumn("is_paid", F.when(F.col("status") == "paid", 1).otherwise(0))
)
silver_df.write.mode("overwrite").format("delta").saveAsTable("silver_orders")
Production code should not silently lose invalid rows. Write them to
silver_orders_quarantine with quality_rule, quality_reason,
source_batch_id, and quarantined_at.
5. Publish Gold
Gold is a consumer contract. It should answer a business question, not repeat source cleanup. One Silver table can feed several Gold products with different measures or grains.
gold_df = (
spark.table("silver_orders")
.groupBy("region", "order_month")
.agg(
F.sum("net_amount").alias("total_revenue"),
F.count("order_id").alias("order_count"),
F.sum("is_paid").alias("paid_orders")
)
.orderBy("order_month", "region")
)
gold_df.write.mode("overwrite").format("delta").saveAsTable("gold_orders_by_region")
For larger reporting solutions, Gold commonly contains fact and dimension tables at useful grains. A semantic model can then own relationships, measures, and user-facing terminology.
6. Validate the layers
spark.sql("SHOW TABLES").show(truncate=False)
spark.sql("SELECT * FROM gold_orders_by_region ORDER BY order_month, region").show(50, truncate=False)
The checks should also record counts of accepted, rejected, corrected, and late records. Those are operational metrics, not temporary notebook output.
Why Silver exists
Silver is where source records become dependable domain records. It makes decisions about:
- Validity: does the record satisfy schema and business rules?
- Uniqueness: is it new, corrected, or duplicated?
- Identity: which business entity does it reference?
- Consistency: are units, currencies, time zones, and codes aligned?
- History: how are late updates, deletions, and changing dimensions handled?
If every Gold product repeats those decisions, the organization has multiple definitions of trusted data.
Put quality checks at the boundary
| Boundary | Purpose | Examples | Failure handling |
|---|---|---|---|
| Landing to Bronze | Protect replayability | File readable, expected columns, batch complete | Preserve payload; alert or quarantine |
| Bronze to Silver | Decide record validity | Types, required fields, ranges, duplicates, relationships | Accepted and rejected rows with reasons |
| Silver to Gold | Protect business meaning | Reconciliation, KPI rules, freshness, completeness | Block or flag publication |
| Consumption | Protect the user contract | Measures, RLS, report totals, SLA checks | Alert the product owner |
For orders, check that order_id is present and unique, customer_id resolves,
amount is non-negative, dates are plausible, status is canonical, and accepted
plus rejected rows reconcile to Bronze.
Harmonization belongs mainly in Silver
ERP, e-commerce, and marketplace sources may disagree on customer IDs, product codes, statuses, currencies, units, time zones, and region names. Silver maps those variations to canonical keys and vocabularies using crosswalks, reference data, master-data matching, or survivorship rules.
Keep source keys and original codes beside canonical values. Gold can then apply consumer-specific interpretations without losing traceability.
Do you need a Platinum layer?
Usually, no. Platinum is not part of the standard Bronze-Silver-Gold pattern. Add another serving tier only when it has a distinct contract, owner, security boundary, or operational requirement, such as:
- a regulator-facing dataset;
- a certified cross-domain product;
- an application or API contract;
- a point-in-time machine-learning feature set; or
- a partner-facing product with a release process.
Names such as certified, serving, regulatory, or features can be clearer
than platinum. If you cannot explain the extra owner and contract, Gold is
probably enough.
One Lakehouse or several?
This tutorial keeps everything together for clarity. Production can use:
- separate schemas when ownership is shared;
- separate Lakehouses when lifecycle or access differs;
- separate workspaces when deployment, capacity, or security differs; or
- a Lakehouse for Bronze and Silver plus a Warehouse for SQL-first Gold.
Do not copy data merely to satisfy a diagram. OneLake shortcuts can expose existing data without duplication.
Further reading
- Understand medallion architecture for Fabric with OneLake
- What is a Lakehouse in Microsoft Fabric?
- Implement medallion architecture with materialized lake views
- What is the medallion Lakehouse architecture?
- Delta Lake optimization and V-Order in Fabric
- Consume Azure Databricks Unity Catalog data in Fabric with shortcuts
- Fix missing or stale Lakehouse tables in the Fabric SQL endpoint
- A 100% native Fabric analytics stack with dbt, Airflow, Lakehouse, and Direct Lake
Common pitfalls
- transforming raw data before preserving it;
- hiding business logic in the wrong layer;
- dropping rejected records instead of measuring them;
- aggregating Silver too early;
- repeating harmonization in each Gold product;
- ignoring schema drift;
- publishing without reconciliation; and
- adding layers without a distinct contract.
The takeaway
A medallion Lakehouse is a data operating model:
preserve -> validate and harmonize -> publish for a consumer
Keep Bronze replayable, make Silver dependable at useful detail, make Gold explicit about its consumer, and add another layer only when its contract is genuinely different.
Loading comments…