Azure Lesson 118 of 137

DP-203: End-to-End Azure Data Engineering — Ingest, Store, Transform, Serve & Stream

In a nutshell

Picture a car factory’s assembly line. Raw parts arrive at the loading dock exactly as the supplier shipped them — dented, unsorted, some in the wrong box. They move down the belt and stop at stations: one straightens and inspects, the next bolts sub-assemblies together, the next paints and polishes. A finished car — uniform, trustworthy, ready to sell — rolls off the end. Nobody sells the raw parts, and nobody throws them away either: the dock keeps a record of exactly what turned up, so any car can be rebuilt from scratch if a later station got something wrong.

An end-to-end data pipeline is that assembly line, for data. Raw records arrive from source systems (an app database, a SaaS API, a stream of website clicks) and land untouched in the bronze layer — the loading dock. They flow to silver, where they are de-duplicated, typed and validated (the straightening-and-inspection station). They flow to gold, where they are shaped into the business-ready facts and aggregates a report actually consumes (paint and polish). Finished insight — “yesterday’s sales by region” — rolls off the end into Power BI by 8 a.m. A fast lane runs alongside for events that cannot wait for the overnight shift, driving a live “orders in the last five minutes” tile. And the whole floor is wrapped in the factory’s safety systems: who is allowed in (identity), fences around the site (private networking) and a logbook of where every part came from (governance and lineage).

This lesson is the build guide for that factory on Azure. You will not merely name Data Factory, Synapse, Event Hubs and Fabric — you will wire them together, choose between the realistic options at each station, and be able to say why on a whiteboard and then again on Monday in the portal.

Level: Advanced · Time: ~42 min

Before you start, you want the Azure basics: the tenant → subscription → resource-group hierarchy, how storage accounts work (ADLS Gen2 is a feature of one), and Entra identity + RBAC — because a data platform is identity plus storage plus compute. A little SQL and Python help but are not required; every snippet is explained. The full prerequisite and objective lists follow immediately below.

By the end you will be able to stand up an ADLS Gen2 medallion lake; ingest with Data Factory / Synapse pipelines using the right integration runtime and an incremental/CDC load; transform bronze → silver → gold in Delta with Spark or SQL; bolt on a real-time Event Hubs → Stream Analytics path; serve through dedicated/serverless SQL, Fabric and Power BI; and wrap all of it in orchestration, CI/CD, managed identity, private endpoints and Purview governance.

Exam status — verify before you book. The DP-203 “Data Engineering on Microsoft Azure” exam was retired on 31 March 2025, and with it the Azure Data Engineer Associate credential and its renewal assessments. Microsoft’s current data-engineering certification is DP-700 “Implementing Data Engineering Solutions Using Microsoft Fabric” (the Fabric Data Engineer Associate, which reached GA in November 2024); DP-600 “Fabric Analytics Engineer Associate” covers the modelling-and-serving half. There is no automatic transfer from DP-203 to DP-700 — you sit DP-700 fresh. We keep the DP-203 framing deliberately: the underlying skills are unchanged (pipelines, medallion, Delta, Spark, streaming, serving), and the enormous installed base of Synapse/ADF platforms is exactly what you will operate and migrate for years yet. Every pattern below maps one-to-one onto Fabric, and we flag the Fabric equivalent as we go — so read “DP-203” here as the name of a durable skill set, and sit DP-700 for the badge. (Always confirm the live exam list on Microsoft Learn, as the certification portfolio keeps moving.)

Knowing the names of the Azure analytics services is one thing; being able to assemble them into a working pipeline that ingests yesterday’s orders, cleanses them overnight, joins a live clickstream, and lights up a dashboard by 8 a.m. is another entirely. This lesson is the second of those skills. It is a senior data engineer’s build walkthrough of an end-to-end Azure data platform — the kind of thing you design in an interview on a whiteboard and then implement for real on Monday.

We will follow data through the five stages every platform shares — ingest, store, transform, serve — with streaming running alongside as a first-class path, and orchestration, CI/CD, security and governance wrapped around the whole thing. At each stage we do not merely name the service: we choose between the realistic options (Data Factory vs Synapse pipelines, dedicated vs serverless SQL, Stream Analytics vs Spark Structured Streaming, Synapse vs Microsoft Fabric vs Databricks), say why, and show the az/JSON shape of how it is wired. This is the build-skills companion to the platform overview, and it maps directly to the DP-203: Data Engineering on Microsoft Azure objectives — ingestion and transformation, securing and monitoring data storage and processing, and (increasingly) the Microsoft Fabric-era successor skills.

A note on the 2026 landscape. Microsoft has frozen new investment in standalone Synapse Analytics and made Microsoft Fabric its strategic data platform; the DP-203 exam is being retired in favour of Fabric-centric certifications (DP-700, DP-600). We teach the DP-203 build because the concepts are identical — pipelines, medallion, Delta, Spark, streaming, serving — and the huge installed base of Synapse/ADF estates is exactly what you will operate and migrate. Every pattern here translates one-to-one to Fabric, and we flag the Fabric equivalent as we go.

Learning objectives

By the end of this lesson you can:

Prerequisites

You should be comfortable with the Azure fundamentals — the tenant → subscription → resource group → resource hierarchy, storage accounts (ADLS Gen2 is a storage-account feature), and Entra identity and RBAC, because a data platform is identity plus storage plus compute. Reading SQL and a little Python/PySpark helps but is not required — we explain every snippet. If you want the conceptual map first (what each service is and the Synapse-vs-Fabric-vs-Databricks decision in depth), read the companion Data Factory, Synapse & Microsoft Fabric deep dive — this lesson assumes that map and concentrates on building the pipeline. It sits in the Data Engineering module of the Azure Zero-to-Hero course and is an Advanced lesson; the natural predecessor in the certification ladder is AI-102: Building Production AI.

Core concepts: the shape of an end-to-end pipeline

Before any service, fix the mental model. A data platform is a conveyor belt with five stations. Data is pushed onto the belt by ingestion, parked in the lake, refined by transformation, and handed off to consumers by the serving layer — with a fast lane (streaming) running in parallel for events that cannot wait for the nightly batch.

Stage Job Primary Azure services (Synapse era → Fabric era)
Ingest Pull/push data from sources into the lake Data Factory / Synapse pipelines → Fabric Data Factory, Dataflows Gen2; Event Hubs/IoT (streaming)
Store Hold raw and refined data cheaply, durably, in open formats ADLS Gen2 (Delta/Parquet) → OneLake
Transform Cleanse, conform, aggregate — raw → trustworthy Synapse Spark & SQL pools, Databricks → Fabric notebooks / SQL / Dataflows
Serve Expose curated, modelled data with good performance Dedicated/serverless SQL → Fabric Warehouse/Lakehouse, Power BI (DirectLake)
Stream Process events continuously, low latency Event HubsStream Analytics / Spark Structured Streaming → Fabric Eventstream / Eventhouse (KQL)

Four cross-cutting ideas underpin everything and recur in every interview:

A few terms you will use constantly: OLTP (transactional source systems — your apps, many small writes) vs OLAP (the analytics store — large aggregating reads); CDC (Change Data Capture — capturing only inserts/updates/deletes since last run instead of re-reading the whole table); watermark (the high-water value, e.g. a LastModified timestamp, that marks where the last load stopped); and schema-on-read (the lake imposes structure when you read, not when you write).

Ingest: Data Factory, Synapse pipelines & integration runtimes

Azure Data Factory (ADF) is Azure’s serverless data-integration (ELT orchestration) service — a managed conductor that moves and orchestrates data without storing it. The same engine is embedded in Synapse as Synapse pipelines and in Microsoft Fabric as Fabric Data Factory / Dataflows Gen2; the authoring concepts are near-identical, so learn one and you know all three. Use standalone ADF when integration is the whole job; use Synapse pipelines when ingestion lives next to your Spark/SQL analytics in one workspace; use Fabric for new, BI-led platforms.

The building blocks:

Concept What it is
Pipeline A workflow — an ordered, branching graph of activities
Activity One step (Copy, Data Flow, notebook, stored proc, Lookup, ForEach, If, Until, Web)
Dataset A named, typed view of data in a store (a table, a folder of CSVs)
Linked service A connection string / credential to a store or compute (the “data source”)
Trigger What starts a pipeline — schedule, tumbling window, storage event, or manual
Integration runtime (IR) The compute that actually executes an activity

The integration runtime is the single most-tested ingestion concept because it decides where the work runs and what it can reach:

Integration runtime Where it runs Use it for Gotcha
Azure IR Microsoft-managed, in Azure Cloud-to-cloud copy, public endpoints, Data Flow execution (Spark under the hood) Auto-resolves region; pick region for data residency/latency
Self-hosted IR (SHIR) A VM/host you install inside your network On-prem or private sources (SQL Server, file shares, private endpoints) Dials outbound to ADF (firewall-friendly — no inbound ports); install ≥2 nodes for HA
Azure-SSIS IR Managed cluster of SSIS nodes in Azure Lift-and-shift existing SSIS packages unchanged Always-on cluster — pause/stop when idle or it bills 24×7

Interview classic — “How do you copy data from an on-prem SQL Server into the lake?” Install a Self-hosted Integration Runtime inside the on-prem network; it makes an outbound HTTPS connection to ADF, so no inbound firewall ports are opened. Use it as the IR on the source linked service.

Full vs incremental & CDC

Re-copying an entire table every night does not scale. The two patterns:

On the lake side, both patterns land into Delta and use MERGE (upsert) so a re-run is idempotent — late or duplicate rows update in place rather than appending.

// Copy activity source — incremental query using pipeline parameters
"source": {
  "type": "AzureSqlSource",
  "sqlReaderQuery": "SELECT * FROM dbo.Orders WHERE LastModified > '@{pipeline().parameters.oldWatermark}' AND LastModified <= '@{pipeline().parameters.newWatermark}'"
}

Store: ADLS Gen2, the medallion & Delta/Parquet

The lake is Azure Data Lake Storage Gen2 (ADLS Gen2) — a storage account with the hierarchical namespace (HNS) enabled, which gives real folders and POSIX-style ACLs instead of a flat blob namespace. HNS must be enabled at creation and cannot be toggled on later — a favourite exam gotcha. Organise the lake with the medallion architecture: three layers (containers) of rising data quality.

Layer Also called Contents Format Who writes it
Bronze Raw / landing Data exactly as ingested, immutable, append-only, partitioned by load date Parquet/Delta (or raw CSV/JSON) Ingestion pipelines
Silver Cleansed / conformed De-duplicated, typed, validated, conformed keys; one row per business entity Delta Transform jobs
Gold Curated / serving Business-level aggregates, star-schema facts & dimensions, ready for reporting Delta Transform jobs

File format is a deliberate choice:

Format What it is When
CSV/JSON Row-based text Only at the very edge (raw bronze ingest); never for analytics — slow, untyped, huge
Parquet Columnar, compressed, typed Good general analytics format; great scan/compression
Delta Lake Parquet + a transaction log The default for silver/gold — adds ACID transactions, time-travel, schema enforcement/evolution and efficient MERGE/upserts; the open format Synapse, Fabric and Databricks all share

Partitioning is how you make the lake fast: physically split data into folders by a frequently-filtered column — usually date (/year=2026/month=06/day=15/) — so engines prune (skip) irrelevant partitions instead of scanning everything. Partition on columns you filter on, keep partition counts sane (avoid thousands of tiny partitions), and watch the small-file problem — many tiny files crush performance, so compact them (Delta OPTIMIZE) into ~128 MB–1 GB files.

# Land bronze → write Delta silver, partitioned, with an idempotent upsert
from delta.tables import DeltaTable
df = spark.read.format("delta").load("abfss://bronze@lake.dfs.core.windows.net/orders")
silver_path = "abfss://silver@lake.dfs.core.windows.net/orders"
(DeltaTable.forPath(spark, silver_path).alias("t")
  .merge(df.alias("s"), "t.order_id = s.order_id")
  .whenMatchedUpdateAll().whenNotMatchedInsertAll().execute())   # idempotent

Transform (batch): Synapse Spark, SQL pools & Databricks

Transformation turns bronze into trustworthy silver and curated gold. You have three engines; choosing well is a core DP-203 skill.

Engine Language Best for Billing model Pick it when
Synapse Spark pool PySpark/Scala/Spark SQL Big, complex, semi-structured transforms; ML feature prep Per vCore-hour while the pool is active (auto-pause when idle) Heavy/flexible engineering inside Synapse
Synapse SQL — serverless T-SQL over lake files Ad-hoc exploration, light transforms, CREATE EXTERNAL TABLE/CETAS Per TB scanned, no infra Intermittent SQL over the lake; cheapest for occasional queries
Synapse SQL — dedicated pool T-SQL, MPP warehouse Steady, heavy warehouse loads and serving Provisioned DWU, billed 24×7 (pause when idle) Predictable high-volume warehouse workloads
Azure Databricks PySpark/Scala/SQL, Photon Premier Spark/ML/AI lakehouse; multi-cloud; large-scale engineering Per DBU + VM Serious data engineering/data science, multi-cloud, or where Synapse Spark falls short

The realistic 2026 pattern: Spark (Synapse or Databricks) for the bronze→silver→gold transforms, serverless SQL for cheap ad-hoc exploration, and a dedicated pool or Fabric Warehouse only when a high-concurrency relational serving layer is required. Synapse Spark and Mapping Data Flows (ADF’s visual, code-free Spark) cover most cleansing; drop to notebooks for complex logic.

Typical silver→gold transform steps: deduplicate, enforce/cast types, conform keys (map source codes to canonical dimension keys), handle slowly-changing dimensions (SCD), then aggregate into gold facts. Spark SQL or T-SQL both express this; the lake’s Delta format gives you MERGE for SCD Type-2 history.

Interview classic — “Dedicated vs serverless SQL pool?” Dedicated is a provisioned, always-on MPP warehouse sized in DWUs — fast and predictable for steady heavy loads, but you pay 24×7 so pause it when idle. Serverless queries lake files on demand and bills per TB scanned with no infrastructure — ideal for ad-hoc and intermittent work.

Stream: Event Hubs + Stream Analytics / Spark Structured Streaming

Some data cannot wait for the nightly batch — fraud signals, IoT telemetry, clickstream. The streaming fast lane has three parts: an ingestion buffer, a stream processor, and a sink (usually the lake and/or a hot store).

Azure Event Hubs is the ingestion buffer — a massively-scalable, Kafka-protocol-compatible event broker that absorbs millions of events/sec into partitions and retains them for a window so consumers can read at their own pace. (IoT Hub sits in front for device fleets; Event Hubs Capture can auto-archive raw events straight to the lake as bronze.) Then you pick a processor:

Azure Stream Analytics (ASA) Spark Structured Streaming (Synapse/Databricks)
Language SQL-like query PySpark/Scala
Ops model Fully managed, serverless (Streaming Units) You run a Spark cluster/job
Best for Quick, declarative windowed aggregations, routing, alerting Complex logic, ML scoring, joining streams to large lake tables, unified batch+stream code
Windowing Tumbling, hopping, sliding, session, snapshot Tumbling/sliding + custom; watermarks for late data
Sinks Power BI (live), SQL, ADLS, Cosmos, Event Hubs, Functions Anything Spark writes — Delta lake, DB, etc.

Windowing is the heart of stream processing — you cannot aggregate an infinite stream without bounding it in time:

Window Behaviour Example
Tumbling Fixed, non-overlapping buckets Count orders per 5-minute block
Hopping Fixed size, overlapping by a hop 10-min totals emitted every 5 min
Sliding Emits when events enter/leave the window Alert if >100 events in any 1-min window
Session Groups bursts separated by a gap of inactivity A user’s browsing session

Two more must-knows: a watermark tells the engine how long to wait for late-arriving events before closing a window (trade latency vs completeness), and exactly-once processing (checkpointing + idempotent sinks, e.g. Delta) prevents double-counting on failure/replay. In Fabric, this whole path becomes EventstreamEventhouse (KQL).

-- Azure Stream Analytics: orders per product per 5-minute tumbling window
SELECT productId, System.Timestamp() AS windowEnd, COUNT(*) AS orders
INTO   [powerbi-output]
FROM   [eventhub-input] TIMESTAMP BY eventTime
GROUP  BY productId, TumblingWindow(minute, 5)

Serve: dedicated SQL, Microsoft Fabric & Power BI

The serving layer exposes gold data to humans and tools with good performance and security. Options:

Serving surface What it is When
Synapse serverless SQL Pay-per-TB T-SQL over gold Delta/Parquet (views, external tables) Cheap, intermittent BI/ad-hoc on the lake
Synapse dedicated SQL pool Provisioned MPP warehouse; load gold into relational tables High-concurrency, low-latency enterprise reporting
Microsoft Fabric Warehouse / Lakehouse SQL endpoint SaaS warehouse over OneLake The 2026 default for new platforms
Power BI The BI/visualisation layer on top of any of the above Always — the dashboard the business actually sees

Power BI storage modes are the serving decision most often probed: Import (data copied into Power BI — fastest, but stale until refresh), DirectQuery (queries the source live — fresh, but slower and load-heavy), and DirectLake (a Fabric-only mode that reads Delta tables directly from OneLake in memory — Import-grade speed with live data, no import refresh or DirectQuery round-trip). DirectLake resolves the classic Import-vs-DirectQuery trade-off and is a key reason new platforms target Fabric.

Interview classic — “A report must show live lake data at near-Import speed. What do you choose?” DirectLake mode on Microsoft Fabric — it reads Delta straight from OneLake, giving Import-speed performance on live data.

Orchestrate, CI/CD, secure & govern

A pipeline that only runs when you click “Debug” is a prototype. Production needs the wrapper.

Orchestration. Pipelines are driven by triggers: schedule (wall-clock), tumbling window (fixed, dependency-aware, back-fillable slices — the right choice for incremental loads), storage-event (a new blob arrives), and manual. Inside a pipeline you express dependencies (activity success/failure/completion/skip), loops (ForEach, Until), branching (If/Switch) and parameters; chain pipelines with Execute Pipeline. A master pipeline typically: ingest → transform → load serving → refresh Power BI dataset, each step gated on the previous succeeding.

CI/CD. Never author straight into the Live (published) mode in production. The flow:

  1. Connect the ADF/Synapse workspace to a Git repo (Azure Repos or GitHub) — work happens on feature branches in collaboration mode.
  2. Merge to the collaboration branch; publish generates ARM templates (the adf_publish/workspace_publish branch) describing every pipeline, dataset and linked service.
  3. A release pipeline (Azure DevOps/GitHub Actions) deploys those ARM templates to dev → test → prod, overriding environment-specific values (connection strings, IRs) via parameters/override files. Infra (storage, Synapse, Key Vault, networking) is itself defined in Bicep/Terraform.
# Provision the lake (HNS on) and a Synapse workspace — infra-as-code outline
az storage account create -n kvlakedp203 -g rg-data \
  --sku Standard_LRS --kind StorageV2 --hns true            # HNS = ADLS Gen2
az synapse workspace create -n kv-syn-dp203 -g rg-data \
  --storage-account kvlakedp203 --file-system lake \
  --sql-admin-login-user sqladmin --sql-admin-login-password '<pwd>' \
  --location southindia

Security. Apply Zero-Trust to data: authenticate every hop with managed identity / Microsoft Entra (never account keys or passwords in pipelines — secrets live in Key Vault and are referenced by linked services); enforce least privilege with RBAC + ADLS ACLs; keep traffic off the public internet with private endpoints (and a managed VNet with managed private endpoints for Synapse/ADF) or a Self-hosted IR for on-prem; encrypt at rest (platform or customer-managed keys) and in transit. Lock storage account public network access down and reach it via Private Link.

Governance. Microsoft Purview provides the catalogue and control plane: automatically scan sources to build a discoverable data map, classify and sensitivity-label PII, trace end-to-end lineage (source → transform → report), and centralise access policies and insights — the cure for an ungoverned “data swamp”. Lineage in particular is what lets you answer “where did this number in the board report come from?”.

End-to-end Azure data engineering pipeline

The diagram traces a single record’s journey: an on-prem source crosses a Self-hosted IR into bronze, batch and streaming paths converge through silver into gold Delta tables, and the dedicated SQL pool / Fabric Warehouse serves Power BI — all wrapped by orchestration, managed identity, private endpoints and Purview.

Going deeper

The sections above are the what and why of each station. This one is the machinery inside the stations — the internals that separate an engineer who can draw the pipeline from one who can keep it fast, correct and cheap at 3 a.m. Skip it on a first read; come back when a MERGE is slow, a stream is double-counting, or a dedicated pool bill has quietly doubled.

How Delta actually gives you ACID on plain object storage

ADLS Gen2 is “just” object storage — it has no transactions of its own. Delta bolts them on with a transaction log in a hidden _delta_log/ folder beside the data. Every write appends an ordered, numbered JSON commit (00000000000000000001.json) listing which Parquet files were added and removed; every ten commits Delta writes a Parquet checkpoint so a reader never has to replay the whole history. A reader computes the table’s current state by reading the latest checkpoint plus the JSON commits after it — the log, not a file listing, is the source of truth. This is what gives you time-travel (VERSION AS OF n reads the state at commit n) and ACID: writers use optimistic concurrency — two jobs may prepare commits simultaneously, but the log accepts them one at a time and a conflicting writer retries against the new snapshot.

A MERGE is therefore not an in-place edit: it rewrites whole Parquet files that contain matched rows and records the swap in the log (copy-on-write). That is why a merge touching a few rows can rewrite gigabytes if those rows are scattered — and why partitioning and clustering matter so much. Modern engines soften this with deletion vectors (merge-on-read: mark rows deleted in a side file, rewrite lazily), but availability varies by engine and Delta version, so treat it as an optimisation to confirm, not assume. Three maintenance verbs keep a Delta table healthy: OPTIMIZE compacts many small files into ~128 MB–1 GB ones; ZORDER BY (or Databricks liquid clustering) co-locates rows by a high-selectivity column so min/max statistics in the log let engines skip files (data skipping); and VACUUM physically deletes files no longer referenced, past a retention window (7 days by default) — run it too eagerly and you break time-travel and any in-flight readers.

Dedicated SQL pool internals: distributions, DWU and data movement

A dedicated SQL pool (Synapse) or a Fabric Warehouse is an MPP engine: your table is sliced across exactly 60 distributions, and compute nodes (sized by DWU) are mapped across them. The single biggest performance lever is how each table is distributed, because a join whose keys live on different nodes forces data movement (a shuffle) — the classic cause of a query that “worked in dev, crawls in prod”.

Distribution How rows are placed Use for Cost if wrong
Hash By a hash of a chosen column Large fact tables; hash on the common join/aggregation key Skew (one hot value) or joining on a non-distribution key → shuffle
Round-robin Evenly, at random Staging/landing tables, or when no good hash key exists Every join needs movement — never for a big fact you join often
Replicated Full copy on every node Small dimensions (< ~2 GB) A “small” dim that grows re-broadcasts on every change — watch the size

The rule interviewers probe: hash-distribute big facts on the join key, replicate small dimensions, round-robin only staging. Then reach for the rest of the toolbox — statistics (create/refresh them; the optimiser is blind without them), resource classes / workload management (governing memory and concurrency), result-set caching and materialized views for repeated aggregations, and a high-throughput bulk load via COPY INTO or PolyBase rather than row-by-row inserts. Everything here bills 24×7 once provisioned, so pause the pool when idle — the cheapest tuning is the pool that isn’t running.

Streaming for real: exactly-once, checkpoints and the late-data dial

Under the streaming diagram sit some sharp edges. Event Hubs parallelism and ordering are both the partition: order is guaranteed only within a partition, and a consumer group is one independent reader’s view with its own saved offset (checkpoint). Throughput is bought in Throughput Units (auto-inflate to a cap) on Standard, Processing Units on Premium. A stream processor can only go as parallel as the partitions feeding it.

Azure Stream Analytics scales in Streaming Units, but you only get true parallelism when the job is embarrassingly parallel — input partition key, PARTITION BY, and output all aligned; otherwise SUs sit idle while one partition bottlenecks. Spark Structured Streaming earns exactly-once from three things together: a durable checkpoint location (offsets + a write-ahead log of what was planned), replayable sources (Event Hubs/Kafka), and an idempotent sink (Delta commits are atomic and de-duplicated by the log) — lose the checkpoint directory and you lose the guarantee. The watermark is the dial everyone under-thinks: it tells the engine how long to hold windowed/join state waiting for stragglers before it emits a result and evicts that state; set it long and memory grows and results lag, set it short and genuinely late events are dropped. There is no free lunch — you are choosing where on the latency-vs-completeness line to sit, per query.

Orchestration & CI/CD you only learn in production

The core section gave the happy path; production adds nuance. A tumbling-window trigger is more than “run on a schedule”: its windows are dependency-aware and back-fillable, support retry with a delay, and can declare a self-dependency with a concurrency limit so slice N+1 waits for N — exactly what an ordered incremental load needs. On the release side, never hand-edit the ARM publish branch: the modern flow validates and generates the ARM template in CI with the @microsoft/azure-data-factory-utilities npm package (no manual “Publish” click), then a release stage stops triggers → deploys → restarts triggers (deploying over a running trigger is a well-known way to corrupt a run). Keep every environment-specific value out of the artifacts: global parameters plus Key Vault-referenced linked services mean the same template promotes dev → test → prod with only an override file changing. Synapse workspaces deploy through their own Synapse workspace deployment task rather than ADF’s, and a self-hosted IR can be shared across factories so lower environments reuse one gateway.

Performance and cost engineering

Most Azure data bills are self-inflicted, and the fixes are boringly consistent. File sizing dominates the lake — the small-file problem (thousands of KB-sized files) makes both Spark and serverless SQL crawl because every file is an open/close/list; compact to ~128 MB–1 GB. Serverless SQL bills per TB scanned, so SELECT * over unpartitioned CSV is the expensive anti-pattern: project only the columns you need, filter on a partitioned path so the engine prunes, and store gold as Parquet/Delta (columnar) not CSV — the same query can cost 10× less purely from format and pruning. Spark pools should autoscale and auto-pause, run on right-sized nodes, and lean on Adaptive Query Execution and (where available) Photon to cut vCore-hours. Event Hubs wants auto-inflate rather than a permanently over-provisioned tier. And for steady, predictable spend, reserved capacity (dedicated pools) or a fixed Fabric F-SKU capacity turns variable per-query cost into a flat, plannable one. The one habit that saves the most rupees remains unglamorous: pause and auto-pause everything the instant it is idle, and never leave an Azure-SSIS IR running overnight.

Everything maps to Fabric (the DP-700 direction)

Because Microsoft has frozen new investment in standalone Synapse and made Microsoft Fabric the strategic platform, the durable value of this lesson is that every component has a one-to-one Fabric equivalent. Learn the pattern once; run it on either.

Synapse / ADF component (DP-203) Microsoft Fabric equivalent (DP-700)
ADLS Gen2 storage account (the lake) OneLake (one tenant-wide lake, Delta-native)
Azure Data Factory / Synapse pipelines Data Factory in Fabric (pipelines) + Dataflows Gen2
Synapse Spark pool / notebooks Fabric Spark compute + notebooks
Synapse dedicated SQL pool Fabric Warehouse
Synapse serverless SQL over the lake Lakehouse SQL analytics endpoint
Event Hubs → Stream Analytics EventstreamEventhouse (KQL database)
Power BI Import / DirectQuery DirectLake (Import-speed on live OneLake Delta)
Microsoft Purview (separate) Purview folded into Fabric / OneLake governance

The migration you will actually be paid to do is this table, table row by table row — which is why the “retired” DP-203 skills are the opposite of dead weight.

Hands-on lab

We will build a mini end-to-end pipeline entirely from the az CLI and free/low-cost tiers: create an ADLS Gen2 lake with medallion containers, stand up a serverless SQL endpoint over the lake, run an incremental Copy, and query the result — then clean up. (Spark and dedicated pools cost real money per hour, so the lab uses serverless SQL and tiny data to stay near-free.)

Prerequisites: an Azure subscription and the az CLI logged in (az login).

1. Resource group + ADLS Gen2 lake with medallion containers.

az group create -n rg-dp203-lab -l southindia

az storage account create -n kvdp203lab$RANDOM -g rg-dp203-lab \
  --sku Standard_LRS --kind StorageV2 --hns true            # HNS => ADLS Gen2
# capture the exact name it created:
SA=$(az storage account list -g rg-dp203-lab --query "[0].name" -o tsv)

for layer in bronze silver gold; do
  az storage fs create -n $layer --account-name $SA --auth-mode login
done
az storage fs list --account-name $SA --auth-mode login -o table   # expect 3 rows

Expected: a storage account with isHnsEnabled: true and three filesystems (bronze/silver/gold).

2. Land a tiny bronze dataset (stand-in for an ingested file).

printf 'order_id,product,amount,LastModified\n1,pen,40,2026-06-15T09:00:00Z\n2,book,250,2026-06-15T09:05:00Z\n' > orders.csv
az storage fs file upload -f bronze -s orders.csv \
  -p year=2026/month=06/day=15/orders.csv --account-name $SA --auth-mode login

Expected: the file lands under a date-partitioned path in bronze.

3. Query the lake with serverless SQL (the cheapest transform/serve surface). Create a Synapse workspace (its serverless SQL endpoint is always-on and billed only per TB scanned):

az synapse workspace create -n kvsyndp203$RANDOM -g rg-dp203-lab \
  --storage-account $SA --file-system bronze \
  --sql-admin-login-user sqladmin --sql-admin-login-password 'P@ssw0rd-Change-Me!' \
  -l southindia
az synapse workspace firewall-rule create --workspace-name <wsName> -g rg-dp203-lab \
  -n allowAll --start-ip-address 0.0.0.0 --end-ip-address 255.255.255.255   # lab only!

Then in the Synapse serverless SQL endpoint (Synapse Studio or sqlcmd), run an OPENROWSET over the lake — this is a real “serve straight from the lake” query, no data loaded:

SELECT product, SUM(amount) AS total
FROM OPENROWSET(
  BULK 'https://<SA>.dfs.core.windows.net/bronze/year=2026/month=06/day=15/orders.csv',
  FORMAT='CSV', PARSER_VERSION='2.0', HEADER_ROW=TRUE) AS rows
GROUP BY product;

Expected: two rows — book 250, pen 40.

Validation: you ingested to bronze, partitioned by date, and served an aggregate from the lake with zero provisioned compute — the medallion + serverless pattern in miniature.

Cleanup (important — do this to stop charges):

az group delete -n rg-dp203-lab --yes --no-wait

Cost note (INR). Run end to end and tidied up within the hour this stays in the few-rupees range: ADLS Gen2 LRS storage on this data is effectively nil; serverless SQL bills per TB scanned and this query scans kilobytes (~₹0); the Synapse workspace itself is free (you pay for the pools, which we never started). The expensive things — dedicated SQL pools (DWU, ~₹thousands/day), Spark pools and Stream Analytics SUs — are all avoided here. The one rule that saves real money: pause dedicated pools and auto-pause Spark pools the instant they are idle, and never leave an Azure-SSIS IR running.

Common mistakes & troubleshooting

Symptom Likely cause Fix
“Cannot enable hierarchical namespace” on an existing account HNS is a create-time-only setting Create a new ADLS Gen2 account with --hns true and migrate data
On-prem Copy fails / times out No Self-hosted IR, or it cannot reach ADF Install SHIR inside the network (outbound 443); register it on the source linked service; add a 2nd node for HA
Re-running a load double-counts rows Append instead of upsert; no watermark Use Delta MERGE keyed on the business key + a watermark to load only new rows
Spark/SQL queries are mysteriously slow Small-file problem / no partition pruning OPTIMIZE (compact) Delta tables; partition on the filtered column; avoid one-file-per-record writes
Dedicated SQL pool bill is huge Pool left running 24×7 Pause the dedicated pool when idle; consider serverless for intermittent loads
Stream Analytics job lags / drops events Under-provisioned Streaming Units or skewed Event Hub partitions Scale up SUs; align query partitioning with Event Hub partition key; tune watermark/late-arrival
Pipeline runs in dev but the prod deploy points at dev sources Environment values not parameterised in CI/CD Override linked-service/IR values per environment via ARM template parameters
“Login failed” / key leaked in a linked service Using account keys/passwords instead of identity Switch to managed identity; store any unavoidable secret in Key Vault and reference it

Common beginner mistakes

These are misconceptions rather than symptoms — the wrong mental model that produces a whole class of bugs. (The symptom → cause → fix table above is the companion for when something is already broken.)

Best practices

Security notes

Interview & exam questions

Quick check

  1. Why must hierarchical namespace be decided at storage-account creation, and what do you do if an existing account lacks it?
  2. You need only the rows changed since last night, including deletes, from an Azure SQL source. Which technique, and what lands in the lake to keep re-runs idempotent?
  3. Name the three medallion layers in order and the file format you would use for silver/gold.
  4. A clickstream must drive a live Power BI tile updated every few seconds with a 5-minute rolling count. Which streaming services and which window type?
  5. For a brand-new, Microsoft-centric, BI-led platform in 2026, which serving/platform choice is the strategic default, and which Power BI storage mode gives live data at Import speed?

Answers

  1. HNS is a create-time-only setting — it cannot be toggled on later; you create a new ADLS Gen2 account with HNS enabled and migrate the data.
  2. Native Change Data Capture (CDC) (it captures inserts/updates/deletes); land into Delta and apply with MERGE (upsert), keyed on the business key, so re-runs converge instead of duplicating.
  3. Bronze → silver → gold; use Delta for silver and gold (Parquet + transaction log).
  4. Event HubsStream Analytics (or Spark Structured Streaming) → Power BI live output, using a hopping window (5-minute size, short hop) — or a sliding/tumbling window depending on exact semantics.
  5. Microsoft Fabric (Warehouse/Lakehouse on OneLake) is the strategic default; DirectLake gives live data at Import-grade speed.

Exercise

Design (on paper, then sketch the az/JSON outline) an end-to-end platform for a mid-size retailer with three sources: an on-prem SQL Server of orders, a SaaS CRM exposing a REST API, and a website clickstream of events. Requirements: a single trustworthy analytics store; a sales dashboard refreshed by 8 a.m. daily; a live “orders in the last 5 minutes” tile; and customer PII that must be discoverable and protected.

Produce: (a) the ingest design — which integration runtime per source, and full vs incremental/CDC for each; (b) the lake design — medallion containers, formats, and the partitioning scheme; © the batch transform engine choice with a one-line justification, including how you make the load idempotent; (d) the streaming path — Event Hubs + processor + window type for the live tile; (e) the serving layer and the Power BI storage mode with reasons; (f) the orchestration (trigger types + dependency chain) and CI/CD approach; and (g) three security/governance controls (identity, networking, Purview). Bonus: name one place the small-file problem could bite and how you would prevent it.

Practice challenges

Six exercises, easiest first. Try each before opening the solution. Commands are real and schema-correct for the 2026 surfaces; nothing here needs to be run against a live subscription to be understood, but everything will run if you have one (mind the cost note in the lab). Replace every <placeholder> with your own value — never paste a real subscription ID, key or connection string.

1. (Beginner) Create the lake correctly — and prove HNS is on. Create an ADLS Gen2 storage account and confirm the hierarchical namespace is actually enabled (the setting you cannot add later).

<details> <summary>Solution</summary>

az storage account create -n <lakeacct> -g <rg> \
  --sku Standard_LRS --kind StorageV2 --hns true      # --hns true => ADLS Gen2
az storage account show -n <lakeacct> -g <rg> \
  --query isHnsEnabled -o tsv                          # must print: true

Why: hierarchical namespace is a create-time-only flag — if isHnsEnabled is false you have an ordinary blob account and must recreate, not reconfigure. </details>

2. (Beginner–Intermediate) Write the incremental Copy source. In an ADF/Synapse Copy activity, express a watermark window using pipeline parameters so each run reads only rows changed since the last successful load.

<details> <summary>Solution</summary>

"source": {
  "type": "AzureSqlSource",
  "sqlReaderQuery": "SELECT * FROM dbo.Orders WHERE LastModified > '@{pipeline().parameters.oldWatermark}' AND LastModified <= '@{pipeline().parameters.newWatermark}'"
}

Pattern around it: Lookup old watermark → Lookup MAX(LastModified) as new → Copy the window → write the new watermark to a control table only on success.

Why: a bounded > old AND <= new window plus a persisted watermark makes the load restartable and non-overlapping — a re-run reloads the same window, never a double. </details>

3. (Intermediate) Make silver idempotent with a Delta MERGE. In PySpark, upsert bronze orders into a silver Delta table keyed on the business key so re-running never duplicates.

<details> <summary>Solution</summary>

from delta.tables import DeltaTable
src = spark.read.format("delta").load("abfss://bronze@<lake>.dfs.core.windows.net/orders")
tgt = "abfss://silver@<lake>.dfs.core.windows.net/orders"
(DeltaTable.forPath(spark, tgt).alias("t")
  .merge(src.alias("s"), "t.order_id = s.order_id")
  .whenMatchedUpdateAll()
  .whenNotMatchedInsertAll()
  .execute())

Why: MERGE on the key is an upsert, so a replay converges to the same table (SCD Type-1) instead of appending — the definition of an idempotent load. </details>

4. (Intermediate) Bound a stream with a tumbling window. Write an Azure Stream Analytics query that emits, per product, the order count for each non-overlapping 1-minute window, to a Power BI output.

<details> <summary>Solution</summary>

SELECT productId, System.Timestamp() AS windowEnd, COUNT(*) AS orders
INTO   [powerbi-output]
FROM   [eventhub-input] TIMESTAMP BY eventTime
GROUP  BY productId, TumblingWindow(minute, 1)

Why: TIMESTAMP BY eventTime makes the window event-time based (not arrival time), and a tumbling window gives fixed, non-overlapping buckets — the correct shape for a clean “per minute” count. </details>

5. (Advanced) Choose distributions to kill data movement. In a dedicated SQL pool you load a 2 TB FactSales and a 5 MB DimProduct and join them on ProductKey. Write the CREATE TABLE distribution clauses and justify them.

<details> <summary>Solution</summary>

CREATE TABLE dbo.FactSales ( /* columns... */ )
WITH ( DISTRIBUTION = HASH(ProductKey), CLUSTERED COLUMNSTORE INDEX );

CREATE TABLE dbo.DimProduct ( /* columns... */ )
WITH ( DISTRIBUTION = REPLICATE, CLUSTERED COLUMNSTORE INDEX );

Why: hash-distributing the big fact on the join key co-locates matching rows on the same distribution, and replicating the tiny dimension puts a full copy on every node — so the join needs no shuffle (the number-one cause of slow MPP queries). </details>

6. (Advanced) Fix the small-file problem and control scan cost. A gold Delta table has accumulated thousands of tiny files and queries have slowed. Compact and cluster it, reclaim old files safely, and state the serving-side change that cuts serverless cost.

<details> <summary>Solution</summary>

-- Spark SQL on the Delta table
OPTIMIZE gold.sales ZORDER BY (customer_id);   -- compact to big files + data skipping
VACUUM   gold.sales RETAIN 168 HOURS;          -- remove unreferenced files (>= 7-day default)

Serving side: query gold as Parquet/Delta on a partitioned path and select only needed columns, so serverless SQL prunes partitions and scans less.

Why: OPTIMIZE/ZORDER turns many tiny files into a few large, well-clustered ones (fewer opens, more file-skipping), and because serverless bills per TB scanned, pruning + column projection is a direct rupee saving. Keep VACUUM retention ≥ the default 7 days or you break time-travel and running readers. </details>

Certification mapping

DP-203 (Data Engineering on Microsoft Azure):

Fabric-era successors (the live exams):

(These build skills also underpin AZ-305 “design data storage solutions” scenarios.)

Glossary

Next steps

AzureDP-203Data FactorySynapseStream AnalyticsDelta Lake
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