You have to ingest 500 tables from an on-premises SQL Server into ADLS Gen2 with ADF, incrementally, every night. How do you design one metadata-driven pipeline framework that supports dynamic ingestion and table-wise control?
Why they ask this
EY GDS asks it at L2 with "100+ tables", EY asks a ten-table version, and Deloitte asks what metadata-driven architecture means. It is the single most common Azure design question, because every consultancy project starts with it.
Say this
Keep the list of tables, their watermark columns and last watermarks in a control table; a parent pipeline Looks it up and ForEach-es over it in batches, calling a parameterised child pipeline that copies one table through the self-hosted runtime with a bounded query, then updates that table's watermark only after the copy succeeds. One linked service, one dataset and two pipelines serve all 500 tables.
The reasoning
**The control table is the design.** One row per table: `source_schema`, `source_table`, `load_type` (full or incremental), `watermark_column`, `last_watermark`, `target_path`, `enabled`, `batch_no`, and `last_run_status`. Adding table 501 is an `INSERT`, not a deployment. Keep it in an Azure SQL Database the factory reaches with its managed identity.
**Parent pipeline.** A Lookup reads the table list. A single Lookup returns at most **5,000 rows** and silently truncates beyond that, so 500 is fine; for larger estates, Lookup the batch numbers and let each child Lookup its own tables. A ForEach runs the child in parallel with `batchCount` of, say, 10 — the cap is 50 — chosen for what the source SQL Server and the self-hosted runtime can bear rather than for speed. You cannot nest ForEach loops, which is why the per-table work is an **Execute Pipeline** activity.
**Child pipeline, per table.** A Lookup gets the new high-water mark (`SELECT MAX(modified_at) FROM ...`). A Copy activity on the one parameterised SQL Server dataset runs `SELECT ... WHERE modified_at > '@{pipeline().parameters.last_wm}' AND modified_at <= '@{activity('GetNewWm').output.firstRow.new_wm}'` and writes Parquet to `bronze/<schema>/<table>/load_date=<date>/`. The copy runs on the **self-hosted IR** because the source is private; turn on the SQL Server partition option for the handful of large tables so one copy reads in parallel. Only on success, a Stored Procedure activity writes `new_wm` back and sets status; on failure, it logs the error and leaves the watermark untouched, so the next run picks the same range up again.
**Why bounded ranges.** Reading `> last_wm` without an upper bound lets rows committed during the copy slip past the watermark you record. Fixing the upper bound first makes every run a closed interval that can be rerun exactly. And the watermark column must be reliably set on update — if the application does not maintain `modified_at`, use Change Tracking or CDC for that table instead.
The formulations
CREATE TABLE etl.ingest_control ( source_schema sysname NOT NULL, source_table sysname NOT NULL, load_type varchar(12) NOT NULL, -- 'incremental' | 'full' watermark_column sysname NULL, last_watermark datetime2(3) NULL, target_path varchar(400) NOT NULL, batch_no int NOT NULL, enabled bit NOT NULL DEFAULT 1, last_run_status varchar(20) NULL, CONSTRAINT pk_ingest_control PRIMARY KEY (source_schema, source_table) );
Adding or pausing a table is a row change; the watermark only moves after the copy for that table succeeds.
pl_copy_dbo_customer pl_copy_dbo_orders pl_copy_dbo_order_lines ... 497 more pipelines, datasets and triggers
Five hundred copies of the same logic to review, deploy and fix; a change to the pattern is five hundred changes.
The answer most people give
"A Lookup on INFORMATION_SCHEMA.TABLES, then a ForEach that copies each table with SELECT *." That is a nightly full reload of 500 tables with no per-table control, no watermark and no way to rerun one table — and at 5,001 tables it quietly stops copying the rest.
They’ll ask next
Table 212 fails every night on a type conversion error. How does the framework keep loading the other 499 and make the failure visible?
Reported in 3 interview write-ups
