Azure Data

Designing a Medallion Lakehouse on Data Lake Gen2: Bronze, Silver and Gold Zone Layout

A data lake that nobody trusts is just an expensive folder of files. The pattern that turns a pile of CSVs and JSON dumps into something a business will actually report on is the medallion architecture — three named layers, Bronze, Silver and Gold, each with a clear contract about how clean the data is, who may touch it, and what it is allowed to be used for. On Azure, the storage substrate underneath all three is Azure Data Lake Storage Gen2 (ADLS Gen2): an ordinary storage account with one switch flipped — the hierarchical namespace (HNS) — that turns flat blob “folders” into real directories with atomic renames and POSIX-style access control. Get the zone layout right and every downstream engine (Databricks, Synapse, Microsoft Fabric, even a plain az script) inherits a tidy, governable lake. Get it wrong and you spend the next two years apologising for a raw/ container with four million tiny files and no idea which ones are safe to delete.

This article is the design guide I wish every data engineer read before they created their first container. We treat Bronze, Silver and Gold not as buzzwords but as engineering contracts: Bronze is an immutable, append-only copy of source data exactly as it arrived; Silver is conformed, deduplicated, schema-enforced data that an engineer trusts; Gold is the aggregated, business-shaped tables that a dashboard or finance team consumes. You will learn the storage layout that makes those contracts real — container strategy, folder and partition conventions, file formats (Parquet vs Delta Lake), the RAW/STAGING/CURATED mapping, and the access model (HNS ACLs layered with Azure RBAC) that lets you give the ingestion job write to Bronze and nothing else.

By the end you will be able to stand up the account correctly the first time (HNS on — because you cannot turn it on later), draw the three zones with the right boundaries, choose Parquet or Delta per layer for the right reasons, partition so your queries prune instead of scan, and reason about the bill before it surprises you. Every design choice comes with both the az CLI and a Bicep snippet, real limits and SKUs, and a short troubleshooting section for the failure modes that bite hardest — because the gap between a lakehouse that scales and one that collapses is almost entirely in the layout, not the engine on top.

What problem this solves

Without a layered design, a lake degrades in a predictable way. Someone creates a data container and starts dropping files. Ingestion writes straight into the same place that analysts read from, so a half-finished load is visible to a dashboard mid-run. Nobody knows which copy is canonical, so three teams each build their own “cleaned” version. Schema drift — a source that quietly renames a column or changes a date format — silently corrupts a report, and because the raw bytes were transformed in place, there is no pristine copy to replay from. Six months in, the lake is a swamp: untrusted, un-auditable, and impossible to refactor because everything reads from everything.

The medallion layout solves this by making trust a property of location. Data physically moves through zones, and each move is a checkpoint where quality improves and the contract tightens. Bronze keeps the original bytes forever, so any bug in your transformation logic is recoverable by reprocessing — you never lose the source of truth. Silver is where you enforce schema and dedupe, so consumers downstream never see malformed rows. Gold is small, fast, and business-named, so a Power BI report or a finance query hits a table shaped for it rather than scanning raw events. The zones also give you a natural place to hang access control, retention and cost tiering: lock Bronze down to ingestion identities and tier it to Cool/Cold for cheap long-term retention; open Gold to analysts on Hot for speed.

Who hits the pain without this: any team that started a lake “just to get data landed” and never imposed structure. It bites hardest on regulated workloads (you must prove lineage and who-touched-what), multi-team platforms (everyone writing everywhere), and cost-sensitive shops (raw data on Hot storage with no lifecycle policy is a quiet five-figure annual leak). The medallion pattern is not Azure-specific — it originated with Databricks — but on ADLS Gen2 it becomes concrete: containers, directories, ACLs and lifecycle rules you can express in Bicep and audit in the portal.

To frame the whole article, here is the contract each zone enforces and what breaks if you skip it:

Zone Also called Data state Who writes Who reads What breaks if you skip it
Bronze Raw, Landing Source bytes, untouched, append-only Ingestion only Engineers (rarely) No replay source; a transform bug is unrecoverable
Silver Cleansed, Conformed, Staging Deduped, schema-enforced, typed Transform jobs Engineers, ML, advanced analysts Consumers see malformed/duplicate rows
Gold Curated, Presentation, Serving Aggregated, business-modelled Aggregation jobs BI tools, finance, apps Dashboards scan raw events; slow and costly

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already understand the basics of an Azure storage account — what a container is, the difference between block blobs and files, and the redundancy options (LRS, ZRS, GRS). If those are fuzzy, read Azure Storage Account Fundamentals and Azure Storage Redundancy Decoded: LRS vs ZRS vs GRS vs RA-GRS and How to Choose first. You should be comfortable running az in Cloud Shell, reading JSON output, and reasoning about RBAC role assignments. Familiarity with columnar file formats (Parquet) and SQL helps but is not required — we define the moving parts.

This sits at the storage and layout layer of a data platform, upstream of every processing engine. The medallion zones are where data lives; the engines that move it between zones — Data Factory, Databricks, Synapse, Fabric — sit on top and are covered elsewhere. For the engine that typically lands Bronze, see Build Your First Data Factory Pipeline: Copy Data from Blob Storage into Azure SQL. For the wider catalogue of where each Azure data service fits, The Azure Data Platform Map: Ingest, Store, Process and Serve Across Fabric, Synapse and Databricks is the map this article zooms into.

A quick orientation of which layer owns what, so you know whose problem a given symptom is:

Concern Lives at Owned by This article covers it?
Bytes on disk, folders, ACLs ADLS Gen2 account Platform / data eng Yes — the core
Moving data Bronze→Silver→Gold ADF / Databricks / Fabric Data engineering Mechanism: no; layout it writes to: yes
Table format & ACID Delta / Parquet on the lake Data engineering Yes — format choice
Catalogue, lineage, classification Purview / Unity Catalog Governance Mentioned, not deep
BI semantic model Power BI / Fabric Analytics No (it reads Gold)
Cost tiering & lifecycle Storage account policy Platform / FinOps Yes

Core concepts

Five mental models make every later decision obvious.

A lake is trust expressed as location. The whole point of medallion is that where a file sits tells you how much you can trust it. A path under bronze/ is, by contract, raw and possibly dirty; a path under gold/ is, by contract, validated and business-ready. Engines don’t enforce this — you do, by only ever writing clean data to Silver and only ever aggregating into Gold. The zones are a discipline made physical. Break the discipline (let a job write half-cleaned data into Gold) and the contract is worthless.

ADLS Gen2 is a blob account with a hierarchical namespace. A normal blob container is flat: 2026/06/file.csv is not a folder structure, it is a single blob whose name contains slashes. Listing or renaming a “folder” means scanning and rewriting every blob with that prefix — slow and non-atomic. Hierarchical namespace changes the storage engine so directories are first-class objects: a rename of a directory is a single atomic metadata operation, listings are fast, and you get POSIX ACLs per file and directory. This is the feature that makes the account a data lake rather than just object storage. Crucially, HNS can only be enabled when the account is created — you cannot toggle it on an existing account, so getting this right at creation is the single most important decision in the whole design.

The medallion contract is append-forward, never-rewrite-backward. Data flows one direction: Bronze → Silver → Gold. Bronze is immutable and append-only — you add new files, you never edit old ones, because Bronze is your replay source. Silver is rebuilt from Bronze, Gold from Silver. This directionality is what makes the lake recoverable: if you find a bug in your Silver logic, you fix the code and reprocess from Bronze; you have not destroyed anything. If you ever find yourself “fixing” data by editing Bronze in place, the architecture has already failed.

A table format is a contract layered on files. Raw Parquet is just columnar files in a directory — fast to scan, but with no concept of a transaction, no way to update a single row, and no protection against a reader seeing a half-written dataset. Delta Lake (and the similar Apache Iceberg / Hudi) adds a transaction log — a _delta_log/ directory of JSON + checkpoint files that records every commit. That log buys you ACID transactions, time travel (query the table as of a past version), schema enforcement (reject writes that don’t match), and upserts/deletes (MERGE) — all on plain object storage. Choosing Parquet vs Delta per layer is one of the central design decisions below.

Partitioning is how queries avoid reading everything. A lake table is a directory tree of files. If you organise it as .../event_date=2026-06-24/ (Hive-style partitioning), an engine that filters WHERE event_date = '2026-06-24' reads only that one directory — partition pruning. Pick the wrong partition key (high-cardinality, like user_id) and you create millions of tiny directories and the lake crawls. Pick the right one (a date, a region) and queries are orders of magnitude cheaper. Partition strategy is layout, and layout is this article’s job.

The vocabulary in one table

Pin down every moving part before the deep sections. The glossary at the end repeats these for lookup; this is the mental model side by side:

Term One-line definition Where it lives Why it matters
ADLS Gen2 Blob storage + hierarchical namespace Storage account The lake substrate
HNS Real directories, atomic rename, ACLs Account setting (creation-time) Makes it a lake, not flat blobs
Container Top-level division of the account In the account Often = a zone
Bronze Raw, immutable source copy A container/folder Replay source of truth
Silver Cleansed, deduped, typed data A container/folder What consumers trust
Gold Aggregated, business tables A container/folder What BI/apps read
Delta Lake Transaction log over Parquet _delta_log/ in a table dir ACID + time travel on the lake
Parquet Columnar file format Files in a directory Fast scans, no transactions
Partition Folder-per-value split (date=…) Table directory tree Enables query pruning
ACL POSIX permission on file/dir HNS metadata Fine-grained path access
RBAC Azure role on a scope Account/container scope Coarse data-plane access
Lifecycle policy Auto-tier/delete by age Account management policy Controls Bronze cost
Access tier Hot/Cool/Cold/Archive per blob Blob property Price vs latency trade-off

What makes ADLS Gen2 a lake (not just storage)

The starting point is a StorageV2 account with hierarchical namespace enabled. Everything in the medallion design assumes HNS is on, because three of its capabilities are load-bearing: atomic directory rename (so a job can stage into a temp folder and “publish” by rename), fast recursive listing (so engines enumerate partitions quickly), and POSIX ACLs (so you grant write to one zone without granting the whole account). Without HNS you have a blob account that looks like a lake until you try to rename a directory of a million files and the operation takes hours and is not atomic.

The decision matrix for the account itself:

Setting Recommended for a lake Why Gotcha
Kind StorageV2 Only kind that supports HNS + all features BlobStorage/Storage (classic) do not
Hierarchical namespace Enabled Directories, atomic rename, ACLs Cannot enable later — must be at creation
Performance Standard (Premium for low-latency analytics) Standard fits batch lakes; Premium block blob for high IOPS Premium costs more, no Archive tier
Redundancy ZRS (or GZRS if you need geo) Survives a zone outage in-region LRS is cheapest but single-zone
Default access tier Hot for active, set per-blob later New blobs inherit account default Bronze should move to Cool/Cold via lifecycle
Min TLS version TLS 1.2 Security baseline 1.0/1.1 are deprecated
Public network access Disabled (use Private Endpoint) Keep the lake off the public internet Breaks tools not on the VNet if mis-scoped
Allow shared key access Disabled (prefer Entra ID) Forces identity-based auth, no shared secrets Some legacy tools still need keys
Blob soft delete Enabled (7–30 days) Recover accidental deletes Adds a little storage cost

Create the account with HNS on via CLI:

az storage account create \
  --name kvlakeprod \
  --resource-group rg-lake-prod \
  --location centralindia \
  --sku Standard_ZRS \
  --kind StorageV2 \
  --hns true \
  --min-tls-version TLS1_2 \
  --allow-blob-public-access false \
  --allow-shared-key-access false \
  --default-action Deny

The same in Bicep, which is what you should actually commit so the lake is reproducible:

resource lake 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: 'kvlakeprod'
  location: 'centralindia'
  sku: { name: 'Standard_ZRS' }
  kind: 'StorageV2'
  properties: {
    isHnsEnabled: true            // hierarchical namespace — creation-time only
    minimumTlsVersion: 'TLS1_2'
    allowBlobPublicAccess: false
    allowSharedKeyAccess: false
    supportsHttpsTrafficOnly: true
    networkAcls: { defaultAction: 'Deny' }
  }
}

The single line that matters most is isHnsEnabled: true. If you ship an account without it and only discover later that you needed a lake, your only path is to create a new account and copy everything across with azcopy — there is no in-place upgrade. Treat HNS as a one-way door you walk through at creation. (For bulk copies during such a migration, AzCopy Essentials: Copy, Sync and Resume Large Transfers is the tool.)

ADLS Gen2 vs plain blob, side by side

The differences that actually change your design:

Capability Plain blob (flat) ADLS Gen2 (HNS)
“Folders” Name prefixes only Real directory objects
Rename a directory Copy + delete every blob (slow, non-atomic) Single atomic metadata op
Recursive list Prefix scan Fast directory walk
Per-file/dir permissions No (container-level only) POSIX ACLs
Analytics endpoint blob.core.windows.net also dfs.core.windows.net
Best for Images, backups, static sites Data lakes, analytics
Lifecycle/tiering Yes Yes
Cost Same base, fewer features Same base + ACL metadata

The dfs (Data Lake) endpoint is what analytics engines target; the blob endpoint still works for blob-style operations. One account, two front doors onto the same data.

The three zones as engineering contracts

The heart of the design is treating each zone as a contract with explicit, enforceable terms. Below is what each contract specifies. The discipline is simple to state and easy to violate: only ever write data that meets a zone’s contract into that zone.

Bronze — raw, immutable, replayable

Bronze is a faithful copy of source data, landed exactly as it arrived, and never edited. If a CSV came with a typo’d header, Bronze keeps the typo. If JSON arrived with mixed types in a field, Bronze keeps the mess. The reason is recovery: Bronze is your replay source. Every Silver and Gold table is derived from Bronze, so as long as Bronze is intact you can rebuild everything downstream by re-running code. The cardinal rule is append-only — you add new files (typically partitioned by ingestion date), you never overwrite.

Bronze design choices:

Aspect Bronze choice Rationale
Mutability Append-only, immutable It is the replay source
File format Source format (CSV/JSON) or raw Parquet Preserve fidelity; don’t transform yet
Partitioning By ingestion date (ingest_date=) Matches how you load and expire it
Schema None enforced — schema-on-read Capture even malformed data
Retention Long (often years) Audit + replay; tier to Cool/Cold
Access Write: ingestion identity only; Read: engineers + transform jobs Least privilege
Tier Hot briefly, then Cool/Cold via lifecycle Most Bronze is rarely re-read

Silver — conformed, deduplicated, trusted

Silver is where data becomes trustworthy. The transform job reads Bronze, applies schema enforcement (reject or quarantine rows that don’t match the expected types), deduplicates (a record that arrived twice becomes one), conforms (standardise date formats, units, codes), and often joins reference data to enrich. The output is typed, clean and stable — this is the layer ML pipelines and advanced analysts build on. Silver is usually Delta so you get upserts (MERGE new data into existing tables) and schema enforcement for free.

Aspect Silver choice Rationale
Mutability Updatable via MERGE (Delta) Incremental upserts from Bronze
File format Delta Lake ACID, schema enforcement, time travel
Partitioning By a business/query key (e.g. event_date, region) Match how it’s queried
Schema Enforced — bad rows rejected/quarantined This is where trust begins
Dedup Yes — by natural/business key Consumers must not see dupes
Access Write: transform jobs; Read: eng, ML, analysts Wider read than Bronze
Tier Hot (actively queried) Performance matters here

Gold — aggregated, business-shaped, served

Gold is the presentation layer: small, fast tables modelled for consumption — star schemas, pre-aggregated summaries, KPI tables named in business terms (daily_revenue_by_region, not events_v3). A Power BI report or a finance query hits Gold and reads a few thousand pre-shaped rows instead of scanning billions of raw events. Because Gold is derived and rebuildable from Silver, you can recompute it freely; it does not need the deep retention Bronze does.

Aspect Gold choice Rationale
Mutability Overwrite or MERGE per refresh Recomputed from Silver
File format Delta (or Parquet for read-only marts) ACID if updated; Parquet if append-once
Partitioning Often none or coarse Tables are small; over-partitioning hurts
Schema Strict, business-named columns Consumers depend on stable shape
Granularity Aggregated / modelled Fast for BI
Access Write: aggregation jobs; Read: BI, finance, apps Widest read audience
Tier Hot Latency-sensitive serving

The contracts compared

The whole pipeline on one row each — keep this open while designing:

Property Bronze Silver Gold
Trust level None (raw) High (validated) Highest (business-ready)
Format Source / raw Parquet Delta Delta / Parquet
Schema On-read, none enforced Enforced Strict, named
Mutability Append-only MERGE upserts Overwrite / MERGE
Partition key Ingest date Query/business key None or coarse
Typical volume Largest Medium Smallest
Retention Years Months–years Rebuildable
Tier Cool/Cold Hot Hot
Read audience Engineers Eng + ML + analysts BI + finance + apps

A note on naming: many teams use raw / staging / curated or landing / enriched / serving instead of bronze/silver/gold. The names don’t matter; the contracts do. Pick one vocabulary and use it consistently across containers, code and docs.

Layout: containers, folders and partitions

With the contracts clear, the next decision is physical layout. There are two common strategies, and the choice has real consequences for access control and blast radius.

Container strategy: zone-per-container vs zone-per-folder

Strategy Layout Pros Cons Use when
Zone-per-container bronze/, silver/, gold/ are separate containers Clean RBAC at container scope; clear blast-radius boundary; easy lifecycle-per-zone More containers to manage Most production lakes — recommended
Zone-per-folder One lake container, bronze/silver/gold as top folders Fewer containers; one place Access control must be all-ACL; one bad policy hits all zones Small lakes, single team
Account-per-zone Separate storage accounts per zone Hardest isolation; per-zone limits Cross-account joins; more infra; cost Strict regulatory isolation only

For most teams, zone-per-container wins: you can assign Storage Blob Data Contributor on just the bronze container to your ingestion identity, Storage Blob Data Reader on gold to your BI service principal, and apply a different lifecycle policy per container. Within each container, organise by source/domain then table then partition.

Folder and partition convention

A convention that scales (zone-per-container shown):

bronze/                      # container
  <source-system>/           # e.g. salesforce, web-events
    <entity>/                # e.g. accounts, pageviews
      ingest_date=2026-06-24/
        part-0001.json

silver/                      # container
  <domain>/                  # e.g. sales, marketing
    <table>/                 # e.g. accounts, sessions  (Delta table dir)
      _delta_log/
      event_date=2026-06-24/
        part-0001.snappy.parquet

gold/                        # container
  <subject-area>/            # e.g. finance, product
    <mart-table>/            # e.g. daily_revenue_by_region (Delta)
      _delta_log/
      part-0001.snappy.parquet

Why this shape:

Convention Reason
Source-system first in Bronze Maps to who/what landed it; easy to scope ingestion ACLs per source
Domain/subject-area first in Silver/Gold Maps to how analysts think and to data-product ownership
key=value partition folders (Hive-style) Every engine recognises it → automatic partition pruning
Partition by date in Bronze/Silver Time is the most common filter and a natural retention unit
_delta_log/ present Marks a Delta table; do not write stray files into a table directory

Partition key selection

This is where lakes most often go wrong. The rules:

Rule Why Anti-pattern
Partition by low-cardinality columns Few, large partitions = efficient Partitioning by user_id → millions of tiny dirs
Aim for files ~128 MB–1 GB each Sweet spot for Parquet/Delta scan Thousands of <1 MB files (the small-file problem)
Partition by columns you filter on Pruning only helps if queries use the key Partitioning by a column nobody filters
Keep partition depth shallow (1–2 levels) Deep nesting slows listing year=/month=/day=/hour=/region=/...
Let date be the default partition Most queries are time-bounded No partitioning → full-table scans

A practical heuristic: if a partition column would produce more than a few thousand partitions, or partitions smaller than ~100 MB, it is too granular. Daily date partitions on a table that grows a few GB/day are usually right; switch to monthly if daily produces tiny files, or add hourly only if you genuinely query by hour and volume justifies it.

File format: Parquet vs Delta per layer

The other central design decision is the table format. Both store data as columnar Parquet under the hood; Delta adds a transaction log that turns a directory of files into a table with database-like guarantees.

Capability Raw Parquet Delta Lake
Storage engine Columnar files Columnar files + _delta_log/
ACID transactions No Yes
Concurrent writers Unsafe (last-writer-wins, partial reads) Safe (optimistic concurrency)
Update / delete a row Rewrite the file/partition manually UPDATE / DELETE / MERGE
Schema enforcement No (reader infers) Yes (rejects mismatched writes)
Schema evolution Manual Controlled (mergeSchema)
Time travel (query old version) No Yes (by version or timestamp)
Compaction DIY OPTIMIZE (and Z-ordering)
Reader compatibility Universal Needs a Delta-aware reader
Overhead Minimal Small (log files, checkpoints)

The transaction log is the whole story. Each commit writes a JSON file to _delta_log/ describing which Parquet files were added/removed; periodic checkpoints compact that history. A reader consults the log to know exactly which files form the current (or a past) version — so two jobs writing at once don’t corrupt each other, and a reader never sees a half-written dataset. That is ACID on object storage, which raw Parquet cannot give you.

Per-layer recommendation:

Layer Recommended format Why
Bronze Source format, or raw Parquet Preserve fidelity; you only append; ACID less critical
Silver Delta MERGE upserts, schema enforcement, time travel — the trust layer needs these
Gold Delta (Parquet acceptable for append-once marts) ACID refresh; or simple Parquet if a mart is write-once-read-many

A reasonable simplification for smaller shops: use Delta for Silver and Gold and keep Bronze in whatever the source emits (plus a raw Parquet copy if you want columnar replay). The benefit of Delta grows with concurrency and the need for upserts; if Bronze is strictly append-once partitioned files, raw formats are fine there.

Architecture at a glance

The diagram below traces one record’s journey through the lakehouse, left to right. On the far left, source systems — operational databases, SaaS APIs, and event/clickstreams — emit data. An ingestion layer (Data Factory or a Databricks/Functions job) lands that data, untouched, into the Bronze container of the ADLS Gen2 account, partitioned by ingest date. Inside the account, the hierarchical namespace is what makes the three containers behave like real, governable directories rather than flat blob prefixes. From Bronze, a transform job reads the raw files, enforces schema, deduplicates and conforms, and MERGEs the result into Silver Delta tables partitioned by a business date. A second aggregation job reads Silver and builds the small, business-named Gold Delta tables. On the right, the serve layer — Power BI / Fabric, a SQL endpoint, and ML pipelines — reads Gold (and Silver, for ML) but never touches Bronze.

The badges mark the five places this design most often fails or forces a decision: the creation-time HNS toggle (badge 1), the schema-enforcement gate at the Bronze→Silver boundary (badge 2), small-file accumulation that needs compaction (badge 3), the ACL-plus-RBAC access boundary that keeps ingestion out of Gold (badge 4), and partition pruning on the serving path that decides whether a dashboard query scans a directory or the whole table (badge 5). Notice the arrows only ever flow forward — Bronze → Silver → Gold → serve — which is the directionality that keeps the lake replayable.

Medallion lakehouse on ADLS Gen2: source systems feed an ingestion layer that lands raw data into the Bronze container of a hierarchical-namespace storage account; a transform job enforces schema and merges into Silver Delta tables; an aggregation job builds Gold business tables; and a serving layer of Power BI, SQL and ML reads Gold and Silver, with numbered badges on the HNS toggle, schema gate, small-file compaction, access boundary and partition pruning.

Real-world scenario

Northwind Retail Analytics (a fictional but typical mid-market retailer) ran their reporting off a single ADLS Gen2 container called data. Three teams — web analytics, finance, and merchandising — each wrote into it and each read from it. Their nightly Power BI refresh queried raw clickstream JSON directly, scanning roughly 1.2 TB every run and taking 40 minutes; finance and web analytics had each built a slightly different “cleaned sales” dataset, and the two never quite agreed. Worse, a source change (the e-commerce platform renamed order_total to gross_amount) silently zeroed a revenue column in a board report for two weeks before anyone noticed — and because the ingestion job had transformed the data in place, there was no pristine copy to recompute from. They had a swamp.

The remediation was a medallion redesign over six weeks. They created a new account with HNS enabled (the old one, it turned out, had HNS off — explaining why directory renames in their staging logic took minutes) and three containers: bronze, silver, gold. Ingestion (a Data Factory pipeline) was repointed to write raw JSON into bronze/<source>/<entity>/ingest_date=…/, append-only, on Hot for 30 days then Cool via a lifecycle policy. A Databricks job read Bronze, enforced schema (quarantining rows where types didn’t match into a silver_quarantine path), deduplicated by order ID, and MERGEd into silver Delta tables partitioned by order_date. A second job aggregated Silver into gold/finance/daily_revenue_by_region and gold/product/sessions_daily — a few hundred thousand rows total.

The results were concrete. The nightly Power BI refresh dropped from 40 minutes to under 3, because it now reads a small Gold Delta table instead of scanning 1.2 TB of raw JSON. The schema-rename incident became a non-event: when the source renamed a column again months later, the Silver schema-enforcement step rejected the malformed rows into quarantine and raised an alert the same night, instead of silently corrupting a report — and because Bronze still held the original bytes, they reprocessed cleanly once the mapping was fixed. Access was tightened with RBAC + ACLs: the ingestion identity got Blob Data Contributor on bronze only; the BI service principal got Blob Data Reader on gold only; data engineers read bronze/silver. Storage cost actually fell about 18% despite keeping more history, because the lifecycle policy moved the now-immutable Bronze off Hot to Cool and Cold, and the duplicate “cleaned” datasets were deleted. The single biggest unlock was not performance — it was that all three teams finally reported off the same Gold tables and stopped arguing about whose numbers were right.

Advantages and disadvantages

The medallion pattern on ADLS Gen2 is not free; it adds copies and pipeline stages. The trade-off:

Advantages Disadvantages
Raw data preserved → full replay/recovery Data is stored multiple times (Bronze + Silver + Gold)
Trust is location-based and auditable More pipeline stages to build and operate
Schema enforcement stops bad data spreading Latency: data passes through stages before it’s “Gold”
Clear access boundaries per zone Requires discipline; easy to violate contracts
Cost tiering per layer (cold Bronze, hot Gold) Storing three copies costs more raw bytes
Engine-agnostic (Databricks, Synapse, Fabric all read it) Governance/catalogue still needed on top
Refactor Silver/Gold logic without losing source Small-file and compaction management needed

When each side matters: the advantages dominate for any multi-team, regulated, or report-critical platform — the recoverability and trust-by-location are worth the extra copies, and tiering offsets much of the storage cost. The disadvantages bite hardest for tiny, single-purpose datasets where a full three-zone pipeline is overkill — if you have one source, one consumer, and no compliance need, a single cleaned Delta table may be enough; don’t build three zones for a 10 GB table nobody else touches. The added latency (raw → Bronze → Silver → Gold) matters for near-real-time needs; for those, you keep the medallion zones but stream through them (e.g. structured streaming writing Delta), rather than abandoning the pattern.

Hands-on lab

This lab creates a medallion-shaped lake with az only — no Spark required — so you can see the layout, ACLs and tiering concretely. It uses one small storage account; tear it down at the end. Substitute your own names.

1. Create an HNS-enabled account and the three containers.

RG=rg-lake-lab
ACCT=kvlakelab$RANDOM          # must be globally unique, lowercase
az group create -n $RG -l centralindia

az storage account create -n $ACCT -g $RG -l centralindia \
  --sku Standard_LRS --kind StorageV2 --hns true \
  --min-tls-version TLS1_2 --allow-blob-public-access false

# Create zone containers (filesystems)
for z in bronze silver gold; do
  az storage fs create -n $z --account-name $ACCT --auth-mode login
done

Expected: three filesystems listed by az storage fs list --account-name $ACCT --auth-mode login -o table.

2. Land a Bronze file in a date-partitioned path.

echo '{"order_id":1,"gross_amount":42.50,"order_date":"2026-06-24"}' > order.json

az storage fs directory create -f bronze \
  -n "salesforce/orders/ingest_date=2026-06-24" \
  --account-name $ACCT --auth-mode login

az storage fs file upload -f bronze \
  -s order.json \
  -p "salesforce/orders/ingest_date=2026-06-24/part-0001.json" \
  --account-name $ACCT --auth-mode login

Expected: the file appears under the partitioned directory; az storage fs file list -f bronze -p salesforce/orders --account-name $ACCT --auth-mode login -o table shows it.

3. Set a POSIX ACL — give a group read on Silver, no access to Gold.

# Grant an Entra group read+execute on the silver filesystem root (recursively)
GROUP_OID=$(az ad group show --group "data-analysts" --query id -o tsv)

az storage fs access set-recursive \
  --acl "group:$GROUP_OID:r-x" \
  -p "/" -f silver \
  --account-name $ACCT --auth-mode login

Expected: az storage fs access show -p "/" -f silver --account-name $ACCT --auth-mode login shows the group ACL entry. (r-x = read + traverse; directories need execute to be entered.)

4. Apply a lifecycle policy: move Bronze to Cool after 30 days, Cold after 90, delete after 365.

cat > policy.json <<'EOF'
{
  "rules": [{
    "name": "bronze-tiering",
    "enabled": true,
    "type": "Lifecycle",
    "definition": {
      "filters": { "blobTypes": ["blockBlob"], "prefixMatch": ["bronze/"] },
      "actions": { "baseBlob": {
        "tierToCool":   { "daysAfterModificationGreaterThan": 30 },
        "tierToCold":   { "daysAfterModificationGreaterThan": 90 },
        "delete":       { "daysAfterModificationGreaterThan": 365 }
      }}
    }
  }]
}
EOF

az storage account management-policy create \
  --account-name $ACCT -g $RG --policy @policy.json

Expected: the policy is created; verify with az storage account management-policy show --account-name $ACCT -g $RG.

5. Teardown — delete everything.

az group delete -n $RG --yes --no-wait

What you proved: HNS gives you real directories and partition paths, ACLs scope access per zone/path, and a lifecycle policy controls Bronze cost automatically — the three pillars of the design, without needing a processing engine to see them.

Common mistakes & troubleshooting

The failures below are the ones that recur across real lakes. Each is symptom → root cause → how to confirm → fix.

# Symptom Root cause Confirm Fix
1 Directory rename takes minutes; staging “publish” is slow HNS is off — it’s a flat blob account az storage account show -n <acct> --query isHnsEnabled returns false Recreate the account with --hns true; copy data over with azcopy
2 Queries scan the whole table; costs/time huge No partitioning, or filtering on a non-partition column Check the table dir for key=value folders; check the query’s WHERE vs partition key Partition by the date/region you filter on; rewrite the table partitioned
3 Thousands of tiny files; reads crawl Small-file problem — too many small writes / over-partitioning List file sizes in a partition; many <1 MB files Compact: Delta OPTIMIZE; coarsen partition grain; batch writes
4 A column silently went null/wrong in a report Schema drift — source changed, no enforcement Compare Bronze raw vs Silver; check Silver schema Enforce schema in Silver (Delta); quarantine bad rows; alert
5 App can list a directory but gets 403 reading a file ACL missing execute on parent dirs, or read on the file az storage fs access show -p <path> -f <fs> Grant --x on each parent dir + r-- on the file (or r-x recursive)
6 New user has RBAC role but still 403 on data Confusing RBAC vs ACL, or RBAC propagation delay Check both role assignment and path ACL Assign Blob Data Reader/Contributor and/or set ACLs; wait for RBAC propagation
7 Bill spikes; data is on Hot but rarely read Cold Bronze data left on Hot tier, no lifecycle Check blob tiers; check for a management policy Add a lifecycle policy to tier Bronze to Cool/Cold
8 Reads from “Archive” fail / are unavailable Archive tier is offline — needs rehydration Blob tier shows Archive Rehydrate to Hot/Cool first (hours); don’t Archive actively-read data
9 Two jobs writing same Parquet dir corrupt it Raw Parquet has no transaction control Concurrent writers, partial files Use Delta for any concurrently-written table
10 Half-written data appears in a dashboard Writing in place into a read path (no atomicity) Refresh shows partial rows Write to a temp/staging dir, then atomic rename; or use Delta commits
11 Deleted a file by mistake; it’s gone Soft delete not enabled az storage account blob-service-properties show — soft delete off Enable blob soft delete (7–30 days) before you need it
12 Engine can’t read the lake from a VM Public access Disabled, no Private Endpoint/VNet path Network rules defaultAction: Deny, no PE Add a Private Endpoint or service endpoint for the engine’s subnet

Three reading notes that save the most time:

Best practices

Security notes

The lake holds your most sensitive raw data, so the access model is not an afterthought. ADLS Gen2 gives you two complementary layers, and you usually use both.

RBAC vs ACL — when each applies:

Mechanism Granularity Best for Note
Azure RBAC Account / container scope Coarse “this identity can read/write this container” Control-plane roles (Owner/Contributor) do not grant data access — use Storage Blob Data roles
POSIX ACLs File / directory Fine-grained “this group reads this folder” Layered on top of RBAC; needed for sub-container precision
Both together Default for a real lake RBAC for broad zone access, ACLs for path-level exceptions

The key data-plane roles to know:

Role Grants Use for
Storage Blob Data Reader Read/list blobs BI / analyst read on Gold
Storage Blob Data Contributor Read/write/delete blobs Ingestion (Bronze), transform jobs
Storage Blob Data Owner Full + set ACLs/POSIX Platform admins managing ACLs

Layered guidance:

Cost & sizing

What actually drives a lake’s bill, in rough order: stored capacity (GB-months, tier-dependent), redundancy multiplier, transaction count (per 10,000 operations), and data egress. The medallion pattern stores data multiple times, but disciplined tiering usually makes the total cheaper than an untiered single copy.

Cost driver Driven by How to control
Stored capacity Total GB × tier price Tier Bronze to Cool/Cold/Archive via lifecycle
Redundancy LRS < ZRS < GRS < GZRS Pick the minimum that meets your DR need
Transactions Read/write/list ops per 10k Compact small files (fewer ops); avoid chatty listing
Early-deletion fees Cool/Cold/Archive min-retention Don’t tier data you’ll re-read soon
Egress Cross-region / internet reads Keep compute in-region; Private Endpoint

Access-tier economics (relative — always check current pricing for your region):

Tier Storage price Access price Min retention Use for
Hot Highest Lowest None Gold, active Silver
Cool Lower Higher 30 days Bronze after ~30 days
Cold Lower still Higher still 90 days Older Bronze, rarely read
Archive Lowest Highest (offline) 180 days Compliance retention, almost never read

For a sense of scale: a lake holding ~10 TB of Bronze that is rarely re-read, tiered to Cool/Cold, costs a small fraction of keeping it on Hot — often the single biggest line-item saving in the whole platform. The standing storage cost for tens of TB on Standard is typically a few hundred to low thousands of INR per TB-month depending on tier and redundancy; the transaction and egress costs are usually dwarfed by capacity for a batch lake but can dominate for chatty, small-file workloads (another reason to compact). For the full tier model and worked numbers, see Blob Access Tiers Explained: Hot, Cool, Cold and Archive Cost Trade-offs in Practice, and for the redundancy multiplier, Azure Storage Redundancy Decoded: LRS vs ZRS vs GRS vs RA-GRS and How to Choose.

Sizing rules of thumb:

If… Then
Bronze is rarely re-read after landing Lifecycle to Cool at ~30d, Cold at ~90d
You need in-region zone resilience ZRS (not LRS)
You need cross-region DR GZRS / RA-GZRS (accept higher cost)
Files are <100 MB and numerous Compact before the small-file tax compounds
A mart is small and read constantly Keep it Hot; don’t tier Gold

Interview & exam questions

Q1. What single setting makes a storage account a Data Lake Gen2, and what’s the catch? The hierarchical namespace (HNS), which gives real directories, atomic directory rename and POSIX ACLs. The catch is that it can only be enabled at account creation — you cannot toggle it on an existing account, so you’d have to recreate and copy data. (AZ-305, DP-203)

Q2. Describe the Bronze, Silver and Gold contracts in one sentence each. Bronze is raw, immutable, append-only source data (the replay source); Silver is deduplicated, schema-enforced, conformed data that consumers trust; Gold is aggregated, business-modelled tables shaped for BI and apps. (DP-203)

Q3. Why is Bronze append-only? Because it is the replay source of truth — every Silver/Gold table is derived from it, so if a transform has a bug you fix the code and reprocess from Bronze. Editing Bronze in place would destroy the ability to recover. (DP-203)

Q4. What does Delta Lake add over raw Parquet, and where do you most need it? A transaction log giving ACID transactions, time travel, schema enforcement and MERGE upserts/deletes — all on object storage. You need it most in Silver (upserts and schema enforcement) and Gold (atomic refresh). (DP-203)

Q5. A query scans the entire table and is slow/expensive. What’s the likely layout cause? Either no partitioning or the query filters on a column that isn’t the partition key, so there’s no partition pruning — plus possibly the small-file problem. Partition by the date/key you filter on and compact. (DP-203)

Q6. RBAC vs ACL on ADLS Gen2 — when do you use each? RBAC grants coarse access at account/container scope (use Storage Blob Data roles — control-plane roles don’t grant data access); ACLs grant fine-grained file/directory access layered on top. Real lakes use both: RBAC for broad zone access, ACLs for path-level precision. (AZ-500, DP-203)

Q7. An identity has Contributor on the storage account but gets 403 reading blobs. Why? Contributor is a control-plane role — it can manage the account but does not grant data-plane access. You need a Storage Blob Data role (Reader/Contributor) and/or the right ACL. (AZ-500)

Q8. How do you control the cost of Bronze data that’s rarely re-read? A lifecycle management policy that tiers Bronze blobs to Cool, then Cold, then optionally Archive (or deletes) by age — keeping Gold on Hot. Tiering is the biggest single cost lever in a lake. (AZ-104, AZ-305)

Q9. Why prefer zone-per-container over zone-per-folder? Zone-per-container gives clean RBAC at container scope (grant ingestion write to bronze only), a clear blast-radius boundary, and per-zone lifecycle policies — versus one container where access is all-ACL and one bad policy affects everything. (AZ-305)

Q10. What is the small-file problem and how do you fix it? Too many tiny files (from over-partitioning or many small writes) make reads slow and inflate transaction costs, because each file is a separate I/O. Fix by compacting (Delta OPTIMIZE), coarsening partition grain, and batching writes toward ~128 MB–1 GB files. (DP-203)

Q11. How does the medallion pattern stop schema-drift corruption? Schema enforcement at the Bronze→Silver boundary (a Delta capability) rejects or quarantines rows that don’t match the expected schema and alerts, instead of silently passing bad data downstream — and because Bronze keeps the original bytes, you can reprocess once the source change is mapped. (DP-203)

Q12. Is the medallion pattern Azure-specific? No — it originated with Databricks and applies on any object store. On Azure it becomes concrete as ADLS Gen2 containers/directories with ACLs, lifecycle policies and Delta tables, but the same Bronze/Silver/Gold contracts apply on S3 or GCS. (DP-203)

Quick check

  1. Why must you decide on hierarchical namespace before creating the account?
  2. Which zone is append-only and why?
  3. What does Delta’s _delta_log/ directory give you that raw Parquet cannot?
  4. You partition a table by user_id and reads get slow — what went wrong?
  5. An app has Storage Blob Data Reader on the account but gets 403 listing a directory in gold/. Name two things to check.

Answers

  1. Because HNS can only be enabled at creation — there is no in-place upgrade; fixing it later means recreating the account and copying all data.
  2. Bronze — it is the immutable replay source of truth; every downstream table is derived from it, so you append new files and never edit old ones.
  3. A transaction log, giving ACID transactions, time travel, schema enforcement and MERGE upserts/deletes on object storage.
  4. High-cardinality partitioninguser_id creates millions of tiny directories/files (the small-file problem), so reads must open enormous numbers of files. Partition by a low-cardinality column you actually filter on (e.g. date).
  5. Check (a) whether a path-level ACL is missing — RBAC Reader is account-wide but ACLs can still gate a specific directory, and the directory needs execute to be traversed; and (b) whether a network rule (defaultAction: Deny without the caller’s subnet/PE) is blocking it.

Glossary

Next steps

AzureData Lake StorageADLS Gen2MedallionLakehouseDelta LakeData EngineeringStorage
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading