◐ ai-teacher

🔥 Spark & Databricks · Chapter 8

Medallion architecture in practice

Bronze is your insurance policy, silver is your source of truth, gold is your product. Mix them up and every bug becomes a re-ingest.

Study this lesson free in the app →

The basics

Medallion architecture organises a lakehouse into three layers of tables — bronze, silver, gold — each a cleaner, more useful version of the one before.

Why it exists: if a pipeline goes straight from raw JSON to a revenue dashboard, every bug in parsing, deduplication or business logic is baked into the only copy you have. You cannot replay, cannot tell where a wrong number came from, and every new use case rebuilds the cleaning from scratch.

The layers:

Mental model: a rice mill. Paddy arrives in sacks and is stored as delivered (bronze). It is husked, cleaned and graded (silver). Then it is packed into 5kg retail bags and 25kg restaurant sacks (gold). If a grading machine was miscalibrated, you regrade from the stored paddy — you don't call the farmers.

Words you must know 6

bronzesilvergoldreplayidempotent MERGEquarantine

Explained

Meghalaya Cabs discovered in November that fares since August had been parsed as paise instead of rupees for Tura — ₹2.80 rides instead of ₹280.

Team A had no bronze layer: the raw app events were parsed on arrival and discarded. Recovery meant asking the app team for three months of logs.

Team B kept bronze. They fixed one line in the silver job, replayed August–November from bronze, rebuilt the gold revenue tables, and closed the ticket the same afternoon.

The rice mill

Paddy arrives in sacks from forty farms. The mill stores every sack as delivered, tagged with the farm and date. Nobody cleans it there.

Next, it is husked, de-stoned and graded into a clean store — one bin per variety, every grain accounted for.

Finally it is packed: 5kg bags for shops, 25kg sacks for restaurants, a sample box for the inspector.

When a grading machine turns out to have been miscalibrated for a month, the mill regrades from the stored paddy and repacks. Nobody calls the farmers. That stored paddy is bronze.

Meghalaya Cabs' lakehouse

Arrows point one way. If a consumer needs something new, it is built from silver, never by reaching back into bronze.

What each layer promises

Bronze — completeness. Everything received, in arrival order, untouched: the raw payload (often as a string or VARIANT column) plus _ingest_ts, _source_file, Kafka offset. Append-only. Partitioned by ingest date. Retained as long as you might need to replay.

Silver — correctness. Parsed and typed columns, deduplicated on a producer-generated key, one row per entity at a declared grain, conformed codes (SHG not Shillong/shillong), PII masked or tokenised. Loaded with MERGE, so re-running a batch changes nothing. Bad rows go to quarantine with a reason.

Gold — usefulness. Aggregates, denormalised wide tables, star schemas. Business definitions live here (what counts as 'completed revenue'). Each table has a named consumer, a refresh SLA and an owner.

The contract between layers is what lets you rebuild any layer from the one below.

Bronze → silver, idempotently

# BRONZE: land raw files incrementally (Databricks Auto Loader)
(spark.readStream.format('cloudFiles')
   .option('cloudFiles.format', 'json')
   .option('cloudFiles.schemaLocation', '/chk/bronze_rides/schema')
   .load('s3://mc-landing/ride_events/')
   .withColumn('_ingest_ts', F.current_timestamp())
   .withColumn('_source_file', F.col('_metadata.file_path'))
   .writeStream.option('checkpointLocation', '/chk/bronze_rides')
   .trigger(availableNow=True)
   .toTable('bronze.ride_events'))

# SILVER: latest state per ride, MERGEd so replays are harmless
def upsert_rides(batch, batch_id):
    w = Window.partitionBy('ride_id').orderBy(F.desc('event_ts'))
    latest = (batch.dropDuplicates(['event_id'])
                   .withColumn('rn', F.row_number().over(w))
                   .filter('rn = 1').drop('rn'))
    latest.createOrReplaceTempView('u')
    batch.sparkSession.sql('''
      MERGE INTO silver.rides t USING u s ON t.ride_id = s.ride_id
      WHEN MATCHED AND s.event_ts > t.event_ts THEN UPDATE SET *
      WHEN NOT MATCHED THEN INSERT *''')

The s.event_ts > t.event_ts guard means a late, older event can never overwrite a newer state.

At 100×

Bronze storage is the cheap part. Compressed raw events for Meghalaya Cabs: ~40GB/day, ~15TB/year — about ₹28,000 a month at ₹1.9/GB-month. Losing replayability costs more the first time you need it. Move old bronze to cheaper storage tiers rather than deleting it.

Silver is where compute goes. Dedup and MERGE on billions of rows need the target clustered on the merge key (or partitioned by date with the date in the MERGE condition) so each batch rewrites a few files, not the table.

Gold sprawl is the real risk. Every analyst wants their own table. Without owners, you get 400 gold tables, 30 definitions of 'revenue', and a nightly bill nobody can explain. Prefer a governed set of gold models (dbt or DLT) plus views.

Backfills at scale — replay bronze by partition, in parallel, into silver; then recompute only the gold partitions whose dates changed.

Gotchas

Bronze: keep everything. Silver: get it right once. Gold: shape it for someone.

Fix in the layer that owns the logic, then rebuild downstream from the layer below.

Words to know

BronzeRaw, append-only layer holding data exactly as received plus ingestion metadata; exists for replay and audit.
SilverCleaned, typed, deduplicated, conformed entity tables at a declared grain; the reusable source of truth.
GoldBusiness-level aggregates and dimensional models built for named consumers.
ReplayReprocessing a downstream layer from the layer below after fixing logic, without re-ingesting from sources.
Quarantine tableA table receiving rows that failed quality checks, with the reason, instead of silently dropping them.
Auto LoaderDatabricks' incremental file ingestion source (cloudFiles) that tracks which files are already loaded.
ExpectationsDeclarative data-quality rules (e.g. in Delta Live Tables) that warn, drop or fail on violating rows.

In two minutes

Test yourself

1. What is bronze's primary job?

  1. Serving dashboards quickly
  2. Keeping data exactly as received so any downstream layer can be rebuilt
  3. Enforcing business definitions
  4. Masking PII
Show the answer

Keeping data exactly as received so any downstream layer can be rebuilt — Bronze trades usability for completeness and replayability. Business definitions belong in gold; PII masking usually at silver.

2. Where should deduplication on event_id happen?

  1. Bronze
  2. Gold
  3. Silver
  4. In the BI tool
Show the answer

Silver — Once, in silver, so every consumer inherits it. Deduping in bronze breaks 'as received'; deduping in gold repeats the logic per table.

3. Which table belongs in gold?

  1. daily revenue per city for the finance dashboard
  2. rides parsed and deduplicated, one row per ride_id
  3. raw JSON ride events with _ingest_ts
  4. a quarantine table of malformed events
Show the answer

daily revenue per city for the finance dashboard — Gold is purpose-shaped for a consumer. The deduplicated rides table is silver, raw events are bronze, and quarantine sits beside silver.

4. A bug in the fare-parsing logic is found, affecting the last 90 days. Bronze retains a year. What's the correct recovery?

  1. Patch the wrong numbers directly in gold
  2. Re-ingest 90 days from the app's servers
  3. Edit bronze to correct the raw values
  4. Fix the silver job, reprocess 90 days of bronze into silver, then recompute affected gold partitions
Show the answer

Fix the silver job, reprocess 90 days of bronze into silver, then recompute affected gold partitions — The logic lives in silver, so the fix and replay start there and flow downstream. Patching gold leaves silver wrong for every other consumer; editing bronze destroys the audit trail.

5. The bronze ingest job was retried after a timeout and loaded the same 2 million events twice. Gold revenue for that day doubled. What design flaw let this through?

  1. Bronze should have rejected duplicates
  2. Silver appended instead of MERGEing/deduping on event_id
  3. Gold should use count(distinct)
  4. Auto Loader is not reliable
Show the answer

Silver appended instead of MERGEing/deduping on event_id — Bronze is allowed to hold duplicates — that's 'as received'. Silver's job is to make reprocessing harmless with dedup + MERGE on a business key.

6. An analyst needs a new field that exists in raw events but isn't parsed into silver yet. What should happen?

  1. The gold table reads bronze for that field
  2. The analyst queries bronze directly in the dashboard
  3. Add the field to the silver job (backfill from bronze), then expose it in gold
  4. Create a fourth 'platinum' layer
Show the answer

Add the field to the silver job (backfill from bronze), then expose it in gold — Keeping the flow one-way means silver stays the single place raw data is interpreted. Backfilling from bronze is exactly why bronze kept everything.

There are 8 questions, 3 puzzles and 5 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →