Enterprise Data Architect
Architecture styles, table formats, compatibility, governance, privacy, security and lineage across AWS, Azure, Google Cloud, Snowflake and Databricks, and how to implement lakes, warehouses, lakehouses and data mesh and modernise legacy systems.
1. What a Data Architect Does
A data architect designs how an organisation's data is collected, stored, connected, protected and used, so that many teams can build on it safely. Data engineers build pipelines; the architect decides the rules and the structure those pipelines fit into.
| Role | Main question | Typical output |
|---|---|---|
| Data engineer | How do I move and transform this data? | Pipelines, jobs, tables |
| Data modeler | How should this data be structured? | Logical and physical models |
| Data architect | How does all our data fit together, safely, at scale? | Platform design, standards, policies, decisions |
| Data governance lead | Who owns the data, and which rules apply? | Policies, roles, glossary |
This card continues the online retailer from the Data Modelling and dbt cards. The sales mart works. But the company has grown to 40 pipelines and three teams, it now holds customer data from India and Europe, and an auditor has asked a simple question: "Who can see customer emails, and where does that data go?" Nobody can answer it quickly. Answering it properly is the architect's job, and each section below solves part of it.
| Responsibility | Covered in |
|---|---|
| Overall platform design | The platform at a glance |
| Who owns and defines data | Data governance, metadata and catalogs |
| Which data is sensitive | Data classification |
| Legal duties for personal data | Data privacy |
| Who can access what | Data security |
| Where data comes from and goes | Data lineage |
| Whether the data is right | Data quality |
| Recovering from failure, controlling cost | Resilience, lifecycle and cost |
Thinking across the whole enterprise
An enterprise data architect is not tied to one product. A large company usually runs several platforms at once: an ERP on Oracle or SAP, a CRM in the cloud, a warehouse on Snowflake or BigQuery, a lakehouse on Databricks, Kafka for events and Power BI or Tableau for reporting. The architect's job is to make them work together under one set of rules, and to choose new technology for good reasons rather than habit.
| Skill area | What to know |
|---|---|
| Platforms | AWS, Azure and Google Cloud data services; Databricks, Snowflake; on-premise databases |
| Architecture styles | Warehouse, lake, lakehouse, data mesh, data fabric, federation |
| Integration | Batch, CDC, streaming, APIs, replication and virtualisation |
| Governance and security | Catalogs, classification, access models, encryption, audit, privacy law |
| Data management | Modelling, quality, MDM, metadata, lineage, lifecycle |
| Delivery | Cost, migration, vendor selection, roadmaps, stakeholder management |
Frameworks worth knowing
| Framework | What it gives you |
|---|---|
| TOGAF | An enterprise architecture method (the ADM cycle), where data architecture is one of four domains |
| Zachman Framework | A grid for describing an enterprise from different viewpoints |
| DAMA-DMBOK | The body of knowledge for data management, with governance at the centre |
| DCAM (EDM Council) | A capability model for assessing data management maturity |
| Cloud well-architected frameworks | AWS, Azure and Google Cloud guidance on reliability, security, cost and performance |
You rarely follow a framework rigidly, but interviewers expect you to know them, and they give a shared vocabulary with enterprise architects and auditors.
2. The Platform at a Glance
An architect first draws the whole platform, because every later decision has to fit into it. The layers in the middle carry data; the band underneath applies to all of them, which is why governance and security cannot be added to one pipeline at a time.
SOURCES Orders DB CRM ERP Clickstream (Kafka) Partner APIs
│ │ │ │ │
INGESTION CDC, batch connectors, streaming (Kafka, Kinesis, Event Hubs)
│
STORAGE Bronze (raw) ─► Silver (clean) ─► Gold (business)
Open table formats on object storage, or a cloud warehouse
│
PROCESSING Spark, SQL warehouses, dbt, stream processors
│
SERVING BI tools, ML models, data sharing, APIs, reverse ETL
───────────────────────────────────────────────────────────────────────
ACROSS ALL Governance │ Catalog │ Security │ Lineage │ Quality │ Cost
The data movement patterns behind this picture, such as Lambda, Kappa and medallion, are covered in the Data Architecture Patterns card.
The same layers on every platform
The layers stay the same whichever vendor you choose; only the product names change. This map is the most useful single table for an architect working across clouds.
| Layer | AWS | Azure | Google Cloud |
|---|---|---|---|
| Object storage | S3 | ADLS Gen2, OneLake | Cloud Storage |
| Warehouse | Redshift | Fabric Warehouse, Synapse | BigQuery |
| Spark and lakehouse | EMR, Glue | Fabric Spark, Azure Databricks | Dataproc |
| Batch integration | Glue, AppFlow | Data Factory | Dataflow, Data Fusion |
| Change data capture | DMS | Data Factory, Fabric mirroring | Datastream |
| Streaming | Kinesis, MSK | Event Hubs | Pub/Sub |
| Orchestration | MWAA (Airflow), Step Functions | Data Factory | Cloud Composer (Airflow) |
| Catalog and governance | Glue Data Catalog, Lake Formation, SageMaker Catalog | Microsoft Purview | Knowledge Catalog (formerly Dataplex) |
| Identity | IAM, IAM Identity Center | Microsoft Entra ID | Cloud IAM |
| Keys and secrets | KMS, Secrets Manager | Key Vault | Cloud KMS, Secret Manager |
| Sensitive data discovery | Macie | Purview | Sensitive Data Protection |
| Audit | CloudTrail | Azure Monitor, activity logs | Cloud Audit Logs |
| BI | QuickSight | Power BI | Looker |
| Machine learning | SageMaker | Azure Machine Learning | Vertex AI |
Databricks and Snowflake run on all three clouds, and Kafka (or Confluent), Airflow, dbt and Tableau run almost anywhere. These cross-cloud tools are often chosen precisely to avoid being tied to one cloud.
Cloud product names change often. For example, Google renamed Dataplex to Knowledge Catalog in April 2026, and AWS has moved DataZone into SageMaker. Check current names when writing a design document.
The decisions an architect records
Big choices should be written down with their reasons, as an architecture decision record (ADR). When someone asks in two years why the company chose Delta over Iceberg, the answer is in the record, not in a person's memory.
ADR-007: Use Unity Catalog as the single governance layer
Status: Accepted (2026-03-14)
Context: Three teams manage access separately; an audit could not
list who can read customer emails.
Decision: Register every table in Unity Catalog. Grant access to
groups, never users. Tag personal data with governed tags.
Options: Per-workspace Hive metastore (rejected: no central audit),
external catalog only (rejected: no enforcement).
Impact: All new tables must be Unity Catalog managed tables.
Existing Hive tables are migrated by June.
Non-functional requirements
Most architecture is driven by requirements that are not features. Agree on these numbers early, because they decide cost and design.
| Requirement | Question | Retailer example |
|---|---|---|
| Freshness | How old can the data be? | Sales by 7 a.m.; stock every 5 minutes |
| Availability | How much downtime is acceptable? | 99.5% for dashboards |
| Recovery (RPO) | How much data can we lose? | 1 hour |
| Recovery (RTO) | How fast must we recover? | 4 hours |
| Retention | How long do we keep data? | Orders 8 years; logs 90 days |
| Compliance | Which laws apply? | DPDP Act, GDPR, PCI DSS |
| Scale | How big will it get? | 5 TB now, 40 TB in 3 years |
| Cost | What is the budget? | Cost per team visible each month |
3. Architecture Styles
Before choosing products, an architect chooses a style: where data is stored, who owns it, and how it is accessed. Most enterprises combine several.
| Style | Idea | Strengths | Weaknesses |
|---|---|---|---|
| Data warehouse | Structured, modelled data in one SQL engine | Fast, trusted reporting; mature tooling | Poor for raw, unstructured or ML data |
| Data lake | Store everything as files in cheap storage | Cheap, flexible, any format | Becomes a "data swamp" without governance |
| Lakehouse | Lake storage with warehouse features through open table formats | One copy for BI and ML; ACID on files | Needs discipline in layout and governance |
| Data mesh | Domains own and publish their data as products | Scales ownership across a large company | Needs mature teams and shared standards |
| Data fabric | A metadata-driven layer connecting data wherever it lives | Unified access and governance across systems | Depends on rich metadata; vendor-heavy |
| Federation | Query data in place (virtualisation), without copying | Fast to deliver, no duplication | Slower queries; load on source systems |
Operational and analytical data
| Aspect | Operational (OLTP) | Analytical (OLAP) |
|---|---|---|
| Purpose | Run the business: place orders | Understand the business: revenue trends |
| Queries | Many small reads and writes | Few large scans and aggregations |
| Design | Normalised, row-oriented | Dimensional or wide, column-oriented |
| Examples | PostgreSQL, Oracle, SQL Server, DynamoDB | Snowflake, BigQuery, Redshift, Databricks SQL |
Keeping them separate protects the order system from heavy reports. Data moves from operational to analytical systems through the integration patterns in the next section.
Open table formats: the key to avoiding lock-in
Delta Lake, Apache Iceberg and Apache Hudi add transactions, schema evolution and time travel to Parquet files in your own storage. Because the format is open, several engines can read the same data: Spark, Trino, Snowflake, BigQuery, Athena, Fabric and Databricks all read Iceberg or Delta to some degree. Catalogs that speak the Iceberg REST protocol, such as Apache Polaris, Unity Catalog and Snowflake's catalog, let engines share one table definition. For an architect, this separates the choice of storage from the choice of engine. The next section explains which format to choose.
Compare the formats in the Delta Lake, Apache Iceberg and Apache Hudi cards. Interoperability features change quickly, so check current support for each engine before committing.
4. Medallion Architecture and Data Layers
The medallion architecture organises a lake or lakehouse into layers of increasing quality: bronze (raw), silver (cleaned and conformed) and gold (business-ready). Databricks popularised the name, but the idea is older and platform-neutral: warehouses have long used staging, integration and mart layers, and Snowflake, Fabric, BigQuery and AWS lakes use the same layering under other names.
It is a design pattern, not a product. Each layer has a clear contract: what it contains, who writes it, who may read it, and what quality it guarantees. That contract is what makes data traceable, replayable and trustworthy.
SOURCES -> LANDING -> BRONZE -> SILVER -> GOLD -> SERVING (BI, APIs, ML, AI)
SOURCES LANDING BRONZE SILVER GOLD
databases, SaaS, files exactly raw history, cleaned, typed, business models,
devices, events, as delivered, append-only, deduplicated, KPIs, star
files, APIs short-lived + load metadata conformed keys schemas
quality unknown unknown as the source validated certified
users source teams ingestion jobs engineers engineers, DS analysts, BI
Every layer at a glance
| Layer | Purpose | Typical contents | Write pattern | Retention |
|---|---|---|---|---|
| Edge / source | Where data is produced and first collected | OLTP databases, SaaS apps, IoT devices, logs, partner files | Owned by source teams | Source-defined |
| Landing | A safe drop zone for files exactly as delivered | CSV, JSON, XML, Avro, Parquet, zip files, CDC batches | Write once by ingestion tools; never edited | Days to weeks, then deleted or archived |
| Bronze (raw) | An immutable, queryable copy of every source | Source columns, often as strings, plus load metadata and rescued bad records | Append-only | Long: it is the replay point |
| Silver (cleaned, conformed) | One trusted, deduplicated version of each business entity | Typed, validated, deduplicated tables; SCD history; conformed keys; personal data protected | MERGE / upsert | Long |
| Gold (curated) | Answers to business questions | Star schemas, aggregates, KPIs, wide feature tables, data products | Overwrite or MERGE per partition | As the business needs |
| Serving (optional) | Fast delivery to tools and applications | Semantic models, metric views, caches, low-latency stores, vector indexes | Refreshed from gold | Rebuildable |
Why each layer matters
What each layer does, in detail
| Concern | Bronze | Silver | Gold |
|---|---|---|---|
| Schema | Source schema; schema evolution allowed; often everything as string | Enforced, typed, documented | Business-friendly names, stable contract |
| Quality | None applied: keep bad records (rescued or corrupt column) | Expectations: not null, ranges, referential checks; failures quarantined | Reconciled totals, certified metrics |
| Duplicates | Kept, because they are what the source sent | Removed: latest version per key | None |
| History | Every delivery appended | SCD Type 1 or 2 per attribute | Snapshots and facts as modelled |
| Organised by | Source system | Business entity (customer, order, product) | Business use or domain (sales, finance) |
| Personal data | Restricted access; encrypted | Masked, tokenised or separated | Only what the use case needs |
| Who reads | Data engineers | Engineers, data scientists, power users | Analysts, BI, apps, executives |
| Typical modelling | None | 3NF, Data Vault or cleaned source shape | Kimball star schemas, wide tables, aggregates |
Tools used in each layer
| Layer | Databricks | Snowflake | Open source |
|---|---|---|---|
| Landing | Unity Catalog volumes, cloud storage | Internal or external stages | MinIO, HDFS |
| Into bronze | Auto Loader, Lakeflow Connect, COPY INTO | Snowpipe, Snowpipe Streaming, COPY INTO, Openflow | Kafka Connect, NiFi, Airbyte, Debezium |
| Bronze to silver | Lakeflow Declarative Pipelines, Spark jobs, MERGE | Dynamic Tables, Streams and Tasks, Snowpark | Spark, Flink, dbt |
| Silver to gold | Declarative Pipelines, dbt, Databricks SQL, materialized views | Dynamic Tables, dbt | dbt, Trino, Spark |
| Serving | Databricks SQL, metric views, Lakebase, Vector Search | Semantic views, Cortex Search | Superset, Cube |
| Quality | Declarative Pipelines expectations, Lakehouse Monitoring | Data metric functions | Great Expectations, Soda, dbt tests |
| Orchestration | Lakeflow Jobs | Tasks | Airflow, Dagster |
The same layers on the cloud platforms
| Layer | AWS | Azure / Fabric | Google Cloud |
|---|---|---|---|
| Landing | S3 | ADLS Gen2, OneLake Files | Cloud Storage |
| Into bronze | Glue, Firehose, DMS, AppFlow | Data Factory, Fabric pipelines, Mirroring, Eventstream | Datastream, Dataflow, BigQuery Data Transfer Service |
| Bronze to silver | Glue, EMR | Databricks, Fabric Spark, Data Factory data flows | Dataproc, Dataflow, BigQuery SQL, Dataform |
| Silver to gold | Redshift, Glue, dbt | Fabric Warehouse, Synapse, dbt | BigQuery, Dataform, dbt |
| Serving | Redshift, Athena, QuickSight, OpenSearch | Power BI semantic models (Direct Lake) | BigQuery BI Engine, Looker |
| Quality | Glue Data Quality, Deequ | Purview data quality | Dataplex data quality |
| Orchestration | Step Functions, MWAA | Data Factory, Fabric pipelines | Cloud Composer, Workflows |
Product names move quickly: Databricks renamed Delta Live Tables to Lakeflow Declarative Pipelines and Workflows to Lakeflow Jobs in 2025. Check the current name before an interview, but explain the role of the tool, which does not change.
Tested: a medallion pipeline end to end
We dropped two JSON files into a landing folder. The second file contained a re-sent duplicate, an amount of "abc", a truncated line, and a later update to order 1. Then we built each layer with PySpark and Delta Lake:
from pyspark.sql import functions as F, Window
# BRONZE: read everything as strings, keep bad lines, add load metadata
bronze = (spark.read
.schema("order_id STRING, customer STRING, amount STRING, "
"status STRING, updated_at STRING, _corrupt_record STRING")
.option("columnNameOfCorruptRecord", "_corrupt_record")
.option("recursiveFileLookup", "true") # landing/orders/2026-11-07/*.json
.json("/lake/landing/orders")
.withColumn("_source_file", F.col("_metadata.file_name"))
.withColumn("_ingested_at", F.current_timestamp()))
bronze.write.format("delta").mode("append").save("/lake/bronze/orders")
# SILVER: type, validate, quarantine, keep the latest version per key
br = spark.read.format("delta").load("/lake/bronze/orders")
typed = (br.filter("_corrupt_record IS NULL")
.withColumn("order_id", F.col("order_id").cast("int"))
.withColumn("amount", F.expr("try_cast(amount AS DECIMAL(10,2))"))
.withColumn("updated_at", F.to_timestamp("updated_at")))
bad = typed.filter("amount IS NULL OR amount < 0")
bad.write.format("delta").mode("append").save("/lake/quarantine/orders")
w = Window.partitionBy("order_id").orderBy(F.col("updated_at").desc())
silver = (typed.filter("amount IS NOT NULL AND amount >= 0")
.withColumn("rn", F.row_number().over(w)).filter("rn = 1")
.drop("rn", "_corrupt_record"))
silver.write.format("delta").mode("overwrite").save("/lake/silver/orders")
# GOLD: answer a business question
gold = (silver.groupBy("customer")
.agg(F.count("*").alias("orders"), F.sum("amount").alias("revenue")))
gold.write.format("delta").mode("overwrite").save("/lake/gold/customer_revenue")
| Layer | Rows | What happened |
|---|---|---|
| Landing | 2 files, 8 lines | Stored exactly as delivered |
| Bronze | 8 | Every line kept, including the duplicate and the truncated line, which sits in _corrupt_record with its source file name |
| Quarantine | 1 | Order 5 with amount "abc": try_cast returned NULL instead of failing the job |
| Silver | 4 | Duplicate of order 2 removed; order 1 shows its latest state, SHIPPED at 100.50 |
| Gold | 3 customers | C1: 2 orders, 145.50; C2: 1 order, 80.00; C3: 1 order, 300.00 |
A gotcha we hit: without recursiveFileLookup, reading /lake/landing/orders returned 0 rows with no error, because the files were in a date subfolder that was not named key=value. Spark only discovers subfolders as partitions in that form. Always check row counts after a landing read.
On Databricks: declarative pipelines with expectations
On Databricks the same flow is usually written as a Lakeflow Declarative Pipeline. Auto Loader picks up only new files, and expectations record, drop or fail on bad rows, with the results shown in the pipeline's event log:
import dlt # also: from pyspark import pipelines as dp
from pyspark.sql import functions as F
@dlt.table(comment="Raw orders, exactly as landed")
def bronze_orders():
return (spark.readStream.format("cloudFiles") # Auto Loader
.option("cloudFiles.format", "json")
.option("cloudFiles.schemaLocation", "/Volumes/prod/landing/_schemas/orders")
.option("cloudFiles.schemaEvolutionMode", "rescue") # unknown fields -> _rescued_data
.load("/Volumes/prod/landing/orders/")
.select("*", F.col("_metadata.file_path").alias("_source_file"),
F.current_timestamp().alias("_ingested_at")))
@dlt.table(comment="Clean orders")
@dlt.expect_or_drop("valid_amount", "amount IS NOT NULL AND amount >= 0")
@dlt.expect("has_customer", "customer IS NOT NULL") # record only
def silver_orders_clean():
return (dlt.read_stream("bronze_orders")
.withColumn("amount", F.expr("try_cast(amount AS DECIMAL(10,2))")))
dlt.create_streaming_table("silver_orders")
# create_auto_cdc_flow was called apply_changes before 2025
dlt.create_auto_cdc_flow(target="silver_orders", source="silver_orders_clean",
keys=["order_id"], sequence_by=F.col("updated_at"),
stored_as_scd_type=1)
@dlt.table(comment="Revenue per customer")
def gold_customer_revenue():
return (dlt.read("silver_orders").groupBy("customer")
.agg(F.count("*").alias("orders"), F.sum("amount").alias("revenue")))
On Snowflake: stages, Snowpipe and Dynamic Tables
-- landing: an external stage over cloud storage
CREATE STAGE landing.orders_stage URL = 's3://acme-landing/orders/'
STORAGE_INTEGRATION = s3_int FILE_FORMAT = (TYPE = JSON);
-- bronze: Snowpipe loads each new file as it arrives
CREATE TABLE bronze.orders (raw VARIANT, _source_file STRING, _ingested_at TIMESTAMP_LTZ);
CREATE PIPE bronze.orders_pipe AUTO_INGEST = TRUE AS
COPY INTO bronze.orders
FROM (SELECT $1, METADATA$FILENAME, CURRENT_TIMESTAMP() FROM @landing.orders_stage);
-- silver: a Dynamic Table keeps itself up to date
CREATE DYNAMIC TABLE silver.orders TARGET_LAG = '15 minutes' WAREHOUSE = transform_wh AS
SELECT raw:order_id::INT AS order_id, raw:customer::STRING AS customer,
TRY_TO_DECIMAL(raw:amount::STRING, 10, 2) AS amount,
raw:updated_at::TIMESTAMP_NTZ AS updated_at
FROM bronze.orders
QUALIFY ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY updated_at DESC) = 1;
-- gold
CREATE DYNAMIC TABLE gold.customer_revenue TARGET_LAG = '1 hour' WAREHOUSE = transform_wh AS
SELECT customer, COUNT(*) AS orders, SUM(amount) AS revenue FROM silver.orders GROUP BY customer;
How to lay out the layers
| Option | Example | Good for |
|---|---|---|
| Catalog or database per layer | prod_bronze.crm.customers, prod_silver.sales.orders | Strict isolation and permissions per layer; central platform teams |
| Schema per layer inside a domain catalog | sales.bronze_orders, sales.silver.orders, sales.gold.daily_revenue | Data mesh: each domain owns its own three layers |
| Storage account or bucket per layer | acme-prod-bronze, acme-prod-silver | Separate keys, lifecycle rules and network rules per layer |
Always separate environments (dev, test, prod) first and layers second. Grant users read access to gold by default, silver on request, and bronze to engineers only.
Variations you will meet
| Variation | What it means |
|---|---|
| Other names | Raw / enriched / curated; staging / core / mart; raw / cleansed / conformed / presentation. Same idea. |
| Fewer layers | Small or simple sources may go landing to silver directly, or skip a separate landing zone when Auto Loader or Snowpipe reads the delivery location. Keep bronze if you ever need replay or audit. |
| More layers | A platinum or serving layer for semantic models, feature stores or APIs; a sandbox for experiments; a quarantine table beside silver. |
| Streaming medallion | Every layer is a streaming table, so data flows from bronze to gold in seconds or minutes, with checkpoints providing exactly-once processing. |
| Medallion and modelling | Medallion says where data sits; Kimball, Inmon and Data Vault say how it is modelled. A common enterprise combination is Data Vault or 3NF in silver and Kimball stars in gold. |
Common mistakes
| Mistake | Why it hurts | Better |
|---|---|---|
| Cleaning data in bronze | The original is lost, so a bad rule cannot be undone | Bronze stays raw; clean in silver |
| Silver tables that mirror each source | Every consumer still has to join and reconcile sources | Conform to business entities with shared keys |
| Reports reading silver or bronze | Logic is duplicated and numbers disagree | BI reads gold or the semantic layer only |
| One gold table per report request | Hundreds of near-duplicate tables | Reusable facts and dimensions, metrics in a semantic layer |
| Copying every layer in full every run | Cost and runtime grow with history | Incremental processing: Auto Loader, streams, MERGE, Change Data Feed |
| Treating layers as quality by name | "Gold" means nothing without checks | Explicit tests and owners per table, shown in the catalog |
5. Edge, Ingestion and the Landing Zone
Before the medallion layers begin, data has to travel from where it is produced, the edge, into the platform. This is where most real-world failures start: missing files, duplicates, half-written files, schema surprises, firewalls, and sources that cannot take extra load.
What the edge layer is
The edge is everything outside the central platform that produces or first collects data: factory sensors and gateways, shop tills, mobile apps, branch servers, on-premise databases, partner SFTP servers and SaaS applications. In IoT architectures the edge also has its own compute, an edge gateway, which does useful work before sending anything:
- Buffering when the network drops, so no readings are lost (store and forward)
- Filtering and aggregating, for example sending one-minute averages instead of every 10 ms reading
- Protocol translation from OPC UA, Modbus or MQTT to HTTPS or Kafka
- Compression and encryption before data crosses the internet
- Local decisions such as stopping a machine, without waiting for the cloud
| Edge tool | Platform |
|---|---|
| AWS IoT Greengrass, IoT Core, IoT SiteWise | AWS |
| Azure IoT Edge, IoT Hub, IoT Operations | Azure |
| Partner solutions (Google Cloud IoT Core was retired in 2023), then Pub/Sub | Google Cloud |
| MQTT brokers (Mosquitto, EMQX, HiveMQ), Apache NiFi MiNiFi, Kafka at the edge | Open source and vendor-neutral |
Two meanings of "landing zone"
| Term | What it is | Contains |
|---|---|---|
| Data landing zone | The first storage area where incoming data is dropped exactly as delivered, before bronze | Files and CDC batches in their original format |
| Cloud landing zone | The secure, pre-built cloud foundation that everything runs on | Accounts or subscriptions per environment, network, identity, encryption keys, logging, guardrail policies |
Both matter in an interview. Cloud landing zones are built with AWS Control Tower, the Azure landing zone accelerators (and Cloud Adoption Framework data landing zones), or the Google Cloud landing zone blueprints, usually as Terraform. Build it before any data arrives, because moving networks and permissions later is painful.
How data lands: the ingestion patterns
| Pattern | How it works | Tools |
|---|---|---|
| File drop (push) | The source sends files to an SFTP server or storage bucket | AWS Transfer Family, Azure Blob SFTP, partner SFTP, AzCopy, gsutil and gcloud storage |
| File pull | The platform fetches files or extracts on a schedule | Data Factory, Glue, Storage Transfer Service, DataSync, NiFi |
| Database CDC | Changes are read from the transaction log, with no load on source tables | Debezium, AWS DMS, Datastream, Qlik Replicate, Oracle GoldenGate, Fivetran HVR, Lakeflow Connect, Fabric Mirroring, Snowflake Openflow |
| Bulk database extract | Full or incremental queries by watermark column | Spark JDBC, Data Factory copy, Sqoop (legacy), Airbyte |
| Event streaming | Applications publish events continuously | Kafka, Confluent, Amazon Kinesis and MSK, Azure Event Hubs, Google Pub/Sub |
| SaaS APIs | Managed connectors call the vendor API incrementally | Fivetran, Airbyte, AppFlow, Lakeflow Connect (Salesforce, Workday and others), Data Factory connectors |
| Offline transfer | Petabytes shipped on physical devices | AWS Snowball, Azure Data Box, Google Transfer Appliance |
Connecting the edge to the cloud
| Need | AWS | Azure | Google Cloud |
|---|---|---|---|
| Private dedicated link from a data centre | Direct Connect | ExpressRoute | Cloud Interconnect |
| Encrypted tunnel over the internet | Site-to-Site VPN | VPN Gateway | Cloud VPN |
| Agent inside the private network | DMS replication instance, DataSync agent | Self-hosted integration runtime, on-premises data gateway | Datastream private connectivity |
| Private access to storage | VPC endpoints, PrivateLink | Private endpoints | Private Service Connect |
How data should arrive in the landing zone
1. Agree an ingestion contract per source. Owner, format, schema, delivery frequency, file naming, expected volume, classification and retention. No contract, no onboarding.
2. Use a predictable path. landing/<source>/<dataset>/<yyyy-mm-dd>/<file>_<timestamp>.json, so files are easy to find, replay and expire.
3. Make "complete" explicit. Write to a temporary name and rename, or send a _SUCCESS marker or manifest listing files, row counts and checksums. Never process a file still being written.
4. Make it write-once. Ingestion roles may add files but not change or delete them; object lock or immutability policies where audit requires it.
5. Validate on arrival. Checksum, row count against the manifest, file format, malware scan for external files. Move failures to a rejected folder and alert the source owner.
6. Load into bronze incrementally and idempotently. Auto Loader, Snowpipe or COPY INTO track which files were already loaded, so a re-run never loads a file twice.
7. Expire the landing zone. Once a file is safely in bronze, lifecycle rules delete or archive it after days or weeks. Landing is a buffer, not a store.
Tested: what happens when a load runs twice
Jobs get re-run after failures, so loading from landing into bronze must be idempotent: a re-run must not load the same file again. We put two files of 5 rows each in a landing folder and compared a plain batch load with a streaming load that keeps a checkpoint:
# plain batch: no memory of what was loaded
spark.read.schema(schema).json("/lake/landing/orders") .write.format("delta").mode("append").save("/lake/bronze/orders")
# incremental: the checkpoint records which files were processed
(spark.readStream.schema(schema).json("/lake/landing/orders")
.writeStream.format("delta")
.option("checkpointLocation", "/lake/_checkpoints/bronze_orders")
.trigger(availableNow=True) # process what is there, then stop
.start("/lake/bronze/orders"))
| Run | Batch append | Stream with checkpoint |
|---|---|---|
| First run (10 rows landed) | 10 | 10 |
| Same job again, no new files | 20: every row duplicated | 10: nothing reloaded |
| After a third file with 3 rows | - | 13: only the new file loaded |
Managed tools give the same guarantee: Auto Loader (a streaming read with file tracking that scales to millions of files), Databricks and Snowflake COPY INTO, which skip files they have already loaded unless forced, and Snowpipe, which keeps load history per pipe:
COPY INTO prod_bronze.shop.orders
FROM '/Volumes/prod/landing/shop/orders/'
FILEFORMAT = JSON
FORMAT_OPTIONS ('rescuedDataColumn' = '_rescued_data')
COPY_OPTIONS ('mergeSchema' = 'true'); -- 'force' = 'true' would reload everything
Typical enterprise flow, end to end
Factory sensors -> IoT Edge gateway -(MQTT/HTTPS)-> IoT Hub -> Event Hubs --+
Oracle ERP ---- CDC (GoldenGate / Debezium) -----------------> Kafka ------+-> bronze (streaming)
Salesforce ---- Fivetran / Lakeflow Connect ---------------------------------> bronze
Partner CSVs -- SFTP (Transfer Family) -> s3://landing/partner/ -- Auto Loader -> bronze
|
checksum, manifest, schema check, lifecycle expiry
bronze -> silver (quality, dedupe, conform) -> gold (marts, KPIs) -> Power BI / APIs / ML
6. Choosing an Open Table Format
An open table format turns a folder of Parquet files into a real table: transactions, schema evolution, time travel and efficient updates, while the data stays in your own storage. The choice matters because it decides which engines can read and write your data for years. There is no single winner; there is a right answer for each situation.
| Delta Lake | Iceberg | Hudi | Paimon | |
|---|---|---|---|---|
| Origin | Databricks; now Linux Foundation | Netflix; Apache | Uber; Apache | Flink Table Store; Apache |
| Focus | Simple, fast batch and streaming on Spark | Engine-neutral tables at very large scale | Frequent upserts and incremental processing | Streaming-first on Flink |
| Features | Deletion vectors, liquid clustering, change data feed, UniForm | Hidden partitioning, partition evolution, column IDs, REST catalog standard | Merge-on-read, record-level index, incremental queries, built-in table services | LSM-tree storage for high-rate streaming updates |
| Engines | Databricks, Spark, Fabric | Snowflake, BigQuery, Athena, Trino, Spark, Flink | Spark, Flink | Flink |
| Catalogs | Unity Catalog, Hive | Iceberg REST catalogs (Polaris, Glue, Unity, Horizon), Nessie | Hive, Glue | Paimon catalog, Hive |
Which one, in which scenario
| Scenario | Usual choice | Why |
|---|---|---|
| Databricks is the main platform | Delta | Deepest integration: liquid clustering, predictive optimization, Unity Catalog; UniForm or managed Iceberg for other readers |
| Microsoft Fabric | Delta | OneLake stores tables as Delta; Iceberg can be added through virtualisation |
| Several engines share one copy (Snowflake, Spark, Trino, Athena) | Iceberg | The widest engine support and an open catalog protocol |
| AWS-native lake (Athena, Glue, EMR, Redshift) | Iceberg | First-class support across AWS analytics services |
| Snowflake or BigQuery with external tables | Iceberg | Both support Iceberg tables natively |
| Migrating Hive tables in place | Iceberg | In-place migration, and partition evolution avoids rewrites later |
| Heavy CDC upserts with frequent small updates | Hudi (or Delta/Iceberg with deletion vectors) | Merge-on-read and record-level indexes keep writes cheap |
| Incremental pipelines reading only changes | Hudi, or Delta change data feed | Built-in incremental queries |
| Flink streaming lakehouse | Paimon or Hudi | Designed for continuous streaming writes |
| Small, append-only data read by one engine | Plain Parquet | A table format adds little |
Which engines must WRITE the tables? ├─ Mostly Databricks or Fabric ............... Delta (+ UniForm for readers) ├─ Many engines, or Snowflake / BigQuery ..... Iceberg └─ Flink-heavy streaming ..................... Paimon or Hudi Then check the workload: ├─ Update-heavy CDC with tight latency ....... consider Hudi or MoR └─ Mostly append and large scans ............. any; follow the engines
You do not always have to choose just one
| Tool | What it does | Limit |
|---|---|---|
| Delta UniForm | Writes Iceberg (and Hudi) metadata for Delta tables, so Iceberg engines can read them | Iceberg side is read-only; writes go through Delta |
| Apache XTable (incubating) | Translates metadata between Delta, Iceberg and Hudi over one copy of data | Sync job to run and monitor; feature gaps |
| Managed Iceberg in Unity Catalog or Snowflake | Native Iceberg tables inside the platform | Feature parity varies by platform |
Format versions are a compatibility decision
Formats keep adding features that older readers cannot understand. Delta tracks this with reader and writer protocol features; Iceberg with format versions. Iceberg v3, which adds deletion vectors, row lineage and the VARIANT type, became generally available in Snowflake in May 2026 and was in public preview on Databricks in April 2026. A v3 table is unreadable by engines that only support v2, so upgrade a format version only after every reader supports it.
Details of each format are in the Delta Lake, Apache Iceberg and Apache Hudi cards. Feature support moves quickly; check each engine's documentation for the exact version you run.
7. Data Integration Patterns
Moving data between systems is where most enterprise complexity lives. The right pattern depends on how fresh the data must be and how much load the source can take.
| Pattern | How it works | Freshness | Use when |
|---|---|---|---|
| Batch ETL | Extract, transform on a separate engine, then load | Hours | Legacy warehouses, heavy cleansing before load |
| Batch ELT | Load raw data, transform inside the platform | Hours | Modern warehouses and lakehouses |
| Change data capture | Read the database transaction log for inserts, updates and deletes | Seconds to minutes | Keeping copies of operational databases in sync |
| Event streaming | Applications publish events to a log such as Kafka | Seconds | Clickstreams, IoT, event-driven systems |
| APIs | Call a service for data on request | Real time | SaaS sources, small lookups |
| Replication and mirroring | Copy whole tables or databases continuously | Near real time | Moving operational data into analytics |
| Virtualisation | Query in place across systems | Live | Quick access without new pipelines |
| Reverse ETL | Push analytics results back into business apps | Hours | Sending scores or segments to a CRM |
Tools by pattern
| Pattern | Commercial and cloud | Open source |
|---|---|---|
| ETL and ELT | Informatica, Talend, Fivetran, Matillion, Azure Data Factory, AWS Glue | Airbyte, Apache NiFi, Spark, dbt |
| CDC | Oracle GoldenGate, Qlik Replicate, AWS DMS, Datastream, Fivetran | Debezium with Kafka Connect |
| Streaming | Confluent, Amazon MSK and Kinesis, Event Hubs, Pub/Sub | Apache Kafka, Apache Flink, Spark Structured Streaming |
| Virtualisation | Denodo, Starburst, Dremio | Trino, Presto |
| Reverse ETL | Hightouch, Census | Custom jobs |
| Orchestration | Databricks Jobs, Azure Data Factory, Step Functions | Apache Airflow, Dagster, Prefect |
How the retailer combines them
- Orders database to bronze: CDC, because orders change and finance needs them within minutes
- Website clicks: event streaming through Kafka
- Supplier prices from a partner: an API, pulled hourly
- CRM: batch ELT through a managed connector each night
- Customer churn scores back to the CRM: reverse ETL
CDC captures deletes, which matters for privacy: when a customer is deleted at the source, the delete flows to the analytics copy. Plain batch snapshots miss deletes unless you compare full extracts.
8. Compatibility and Schema Evolution
Data outlives the code that writes it. A schema change that is harmless for the producer can break ten consumers, so an architect defines which changes are allowed, how they are checked, and how breaking changes are rolled out.
The four kinds of compatibility
| Mode | Meaning | Upgrade order |
|---|---|---|
| Backward | Consumers using the new schema can read data written with the old one | Upgrade consumers first |
| Forward | Consumers using the old schema can read data written with the new one | Upgrade producers first |
| Full | Both directions | Any order |
| None | No guarantee | Coordinated release, everyone at once |
Schema registries add transitive variants: the new schema must be compatible with all earlier versions, not only the previous one. This matters when consumers can replay old data, such as a Kafka topic kept for months.
Tested: which changes break readers
We wrote Avro records with one schema and read them with another, in both directions:
| Change | New reader, old data (backward) | Old reader, new data (forward) |
|---|---|---|
| Add a field with a default | OK | OK |
| Add a field without a default | Fails | OK |
| Remove a field that had no default | OK | Fails |
| Remove a field that had a default | OK | OK |
| Widen int to long | OK | Fails |
| Narrow long to int | Fails | OK |
| Rename without an alias | Fails | Fails |
| Rename with an alias | OK | Fails |
| Make a field optional (null by default) | OK | OK while values are present |
The pattern to remember: defaults make changes safe, renames are almost never free, and an alias only helps the new reader. That is why many teams default to full compatibility for shared topics: add optional fields with defaults, and never rename or retype in place.
Schema registries
| Registry | Where |
|---|---|
| Confluent Schema Registry | Kafka and Confluent; Avro, Protobuf, JSON Schema |
| AWS Glue Schema Registry | MSK, Kinesis |
| Azure Schema Registry | Event Hubs |
| Apicurio Registry | Open source |
A registry stores every version of a schema and rejects incompatible versions when the producer registers them, so a breaking change fails at deploy time rather than in a consumer at 3 a.m. Protobuf follows similar rules through field numbers: never reuse or renumber a field.
Tables evolve differently
| Change | Delta Lake | Iceberg | Plain Parquet |
|---|---|---|---|
| Add a column | Yes | Yes | Yes, but readers may miss it |
| Rename a column | Needs column mapping | Yes, columns tracked by ID | Breaks readers |
| Drop a column | Needs column mapping | Yes | Breaks readers |
| Widen a type | Type widening on newer versions | Safe promotions, such as int to long | Risky |
| Change partitioning | Rewrite, or use liquid clustering | Partition evolution without rewrite | Rewrite |
Tested in the other cards: on Delta Lake 3.2, a rename and a drop both failed until column mapping was enabled, and changing INT to BIGINT failed. Appending an extra column to plain Parquet succeeded, but the new column was invisible when reading the folder.
Rolling out a breaking change: expand and contract
1. Expand. Add the new column or field alongside the old one, for example email_address next to email.
2. Dual-write. Producers fill both, so old and new consumers keep working.
3. Migrate consumers. Lineage lists every consumer; each moves to the new field, tracked to completion.
4. Deprecate. Announce a removal date in the catalog and data contract.
5. Contract. Remove the old field once nothing reads it, which lineage and query logs can prove.
Other compatibility an architect manages
| Area | Rule of thumb |
|---|---|
| Data products and APIs | Semantic versioning: a breaking change means a new major version, such as /v2, with the old one kept for an agreed period |
| SQL dialects | Prefer ANSI SQL in shared logic; translate with tools such as SQLGlot when migrating engines |
| File formats | Parquet for analytics, Avro or Protobuf for messages, JSON only at the edges |
| Time | Store timestamps in UTC; convert at display time |
| Codes and encodings | UTF-8 text; ISO 8601 dates, ISO 3166 country codes, ISO 4217 currencies |
| Numbers | DECIMAL for money, never floating point |
| Client libraries | Pin versions; test upgrades in a staging environment against real data |
9. Reliability and Consistency Patterns
Pipelines fail: networks drop, jobs are retried, data arrives late or twice. Reliable architecture does not try to prevent every failure; it makes failure safe to repeat.
| Pattern | Problem it solves | How |
|---|---|---|
| Idempotent writes | A retried job duplicates data | MERGE on a business key, or overwrite a whole partition, instead of plain append |
| At-least-once plus dedupe | Streams can redeliver messages | Deduplicate on an event ID within a time window |
| Exactly-once | Duplicates are unacceptable, such as payments | Checkpointed source offsets plus a transactional or idempotent sink |
| Retries with backoff | Temporary failures | Retry with increasing waits; give up after a limit and alert |
| Dead-letter queue | Bad records block the stream | Divert failures to a separate table or topic for review |
| Replay and backfill | Logic changed or data was wrong | Keep raw data, make jobs parameterised by date, rerun a range |
| Watermarks | Late-arriving events | Wait a bounded time, then finalise; route later data separately |
| Write-audit-publish | Bad data reaching consumers | Write to a hidden area, test it, then publish atomically |
Write-audit-publish on different platforms
| Platform | How to stage, test and publish |
|---|---|
| Iceberg | Write to a branch, validate, then fast-forward main |
| Delta / Databricks | Write to a staging table, test, then swap or MERGE into production |
| Snowflake | Zero-copy clone, load and test the clone, then swap tables |
| Any lake | Git-like branching with lakeFS or Project Nessie |
| dbt | dbt build: tests run after each model and failures stop downstream models |
Consistency across systems
A single Delta, Iceberg or Snowflake table gives ACID transactions. Across systems there is no shared transaction: a row can be in the orders database but not yet in the lakehouse. Architects design for eventual consistency and make the delay explicit, with a freshness SLA per dataset.
- CAP: during a network partition a distributed system must choose between consistency and availability; most data platforms choose availability and catch up later
- Transactional outbox: an application writes the business change and an event row in the same database transaction, and a relay publishes the event, so the two cannot disagree (see the Data Architecture Patterns card)
- Reconciliation: compare counts and totals between source and target daily; it is the safety net for every pattern above
The idempotency patterns are tested in the Data Modelling and dbt cards, where MERGE reruns created no duplicates while plain append did.
10. Scalability, Performance and Workload Isolation
Modern platforms separate storage from compute: data sits once in cheap object storage or a warehouse's storage layer, and any number of compute clusters read it. This is what lets an architect scale and isolate workloads independently.
| Lever | What it does | Examples |
|---|---|---|
| Scale up | A bigger cluster or warehouse for one heavy job | Larger warehouse size, bigger instances |
| Scale out | More clusters for more concurrent users | Snowflake multi-cluster warehouses, Databricks SQL warehouse scaling, Redshift concurrency scaling |
| Workload isolation | Separate compute so ETL never slows dashboards | One warehouse or cluster per workload; BigQuery reservations |
| Data layout | Read less data | Partitioning, clustering, Z-order, sort keys, file sizing |
| Pre-computation | Answer common questions in advance | Materialised views, aggregate tables, BI extracts |
| Caching | Avoid repeating work | Result caches, local SSD caches, semantic-layer caches |
Isolation by workload
one copy of the data
│
┌──────────────┬───────┴──────┬──────────────┐
│ ETL compute │ BI warehouse │ Data science │ Ad hoc SQL
│ scheduled, │ always on, │ GPU or large │ small, auto-
│ job clusters │ auto-scaling │ memory │ suspends
└──────────────┴──────────────┴──────────────┘
each billed and tagged to its own team
Multi-tenancy
| Model | Isolation | Cost | Use when |
|---|---|---|---|
| Separate account or workspace per tenant | Strongest | Highest | Regulated clients, strict contracts |
| Separate catalog or database | Strong | Medium | Business units, data residency |
| Separate schema | Moderate | Low | Teams inside one unit |
| Shared tables with row-level security | Logical only | Lowest | Many small tenants, SaaS analytics |
Capacity planning
Estimate from data volume, growth rate, concurrency and freshness: for example, "5 TB growing 60% a year, 200 dashboard users at 9 a.m., hourly refresh". Load-test with realistic data before go-live, because skew, small files and concurrency show up only at real scale. Then set budgets and alerts, since elastic platforms scale costs as easily as performance.
11. Event-Driven Architecture
Instead of systems asking each other for data, an event-driven architecture has systems publish facts, such as OrderPlaced, to a durable log that many consumers read independently.
| Concept | Meaning |
|---|---|
| Event vs command | An event states something happened (OrderPlaced); a command asks for something (ShipOrder) |
| Topic and partition | Events with the same key go to one partition, which preserves their order |
| Retention and compaction | Keep events for a period, or keep only the latest value per key |
| Event sourcing | Store every change as an event; current state is rebuilt by replaying them |
| CQRS | Separate models for writing and for reading, kept in sync by events |
| Exactly-once in Kafka | Idempotent producers and transactions within Kafka; end to end needs an idempotent sink too |
Design decisions
- Choose the key carefully: ordering is only guaranteed within a partition, so key by the entity whose order matters, such as order_id
- Govern schemas: every topic has a registered schema and a compatibility mode
- Avoid personal data in topics where possible: messages are hard to delete from a retained log, so send IDs and keep details in a governed table
- Plan replay: retention must cover the longest backfill you may need
- Land events in the lakehouse: bronze tables keep the full history for analytics, beyond the topic's retention
12. Enterprise Modelling and the Semantic Layer
Physical models were covered in the Data Modelling card. At enterprise level the architect also standardises meaning, so that "revenue" or "active customer" is the same number in every tool.
| Concept | Purpose |
|---|---|
| Enterprise data model | A high-level map of the main business entities and their relationships across all systems |
| Canonical model | A shared format for data exchanged between systems, so each system maps to one model rather than to every other system |
| Conformed dimensions | One customer, product and date dimension reused by every fact table |
| Reference data | Shared code lists: countries, currencies, status codes |
| Semantic layer | Business metrics defined once, used by every BI and AI tool |
Semantic layer options
| Tool | Notes |
|---|---|
| dbt Semantic Layer (MetricFlow) | Metrics defined in the dbt project |
| Looker (LookML) | Semantic modelling inside Looker |
| Power BI semantic models | Shared datasets in Microsoft Fabric and Power BI |
| Databricks metric views | Metrics defined in Unity Catalog |
| Snowflake semantic views | Metrics and relationships defined in Snowflake |
| Cube, AtScale | Independent semantic layers over many sources |
A semantic layer also matters for AI: natural-language tools such as chat-with-data assistants answer far more reliably when metrics and relationships are defined explicitly rather than guessed from table names.
Standards worth fixing early
- Naming:
snake_case, consistent prefixes such asdim_andfact_, no abbreviations outside an agreed glossary - Keys: surrogate keys in dimensions, business keys kept for tracing
- Time: UTC storage, an explicit time zone column where local time matters
- Money: DECIMAL with currency code; never mix currencies in one column
- Nulls and unknowns: a defined unknown member rather than silent NULLs
13. Challenges in Data Modelling
Drawing a star schema is easy. The hard part is everything that happens around it: unclear requirements, messy sources, changing history, data that arrives late and tables that grow to billions of rows. Interviewers ask about these challenges because they show whether you have modelled real data or only read about it.
The challenges at a glance
| Area | Challenge | What goes wrong | How architects handle it |
|---|---|---|---|
| Requirements | Unclear or changing business questions | Model built for one report, rebuilt for the next | Model business processes, not reports; use a bus matrix; agree definitions in a glossary |
| Grain | Undefined or mixed grain | Double counting, totals that never match | Write the grain as one sentence before any column: "one row per order line" |
| Keys | Same entity, different IDs in each source | One customer counted three times | Surrogate keys, a key-mapping table, MDM golden record |
| History | Attributes change over time | Old sales reported under the new region | SCD Type 1 or 2 per attribute, point-in-time joins |
| Late data | Facts arrive before their dimension rows | Rows dropped by inner joins or keyed to NULL | Inferred (placeholder) members, updated when the real row arrives |
| Late facts | Facts arrive days after the event | Closed periods change silently | Partition by event date, reprocess a rolling window, restatement rules |
| Relationships | Many-to-many: one account, many owners | Fan-out and inflated totals | Bridge tables with allocation weights |
| Hierarchies | Ragged or changing hierarchies | Broken roll-ups in org charts and product trees | Flattened level columns, parent-child with closure table |
| Conformance | Each team builds its own customer dimension | Reports disagree; facts cannot be compared | Conformed dimensions owned once and reused |
| Semantics | "Revenue" means different things to Sales and Finance | Two dashboards, two numbers, no trust | Metrics defined once in a semantic layer |
| Deletes | Sources hard-delete rows | The model keeps ghosts | CDC with delete flags, soft deletes, periodic reconciliation |
| Schema drift | Sources add, rename or retype columns | Pipelines break or load wrong values | Data contracts, schema evolution rules, alerting |
| Scale | Facts with billions of rows | Slow queries, expensive joins | Partitioning, clustering, aggregates, avoid fact-to-fact joins |
| Units | Mixed currencies, time zones, units | Wrong sums and wrong day boundaries | Store UTC plus local date; amount plus currency; convert in one place |
| Privacy | PII spread across many tables | Hard to mask or delete on request | Keep PII in few dimensions, tag it, mask by policy |
| Wide vs star | One big table or star schema? | Wide tables duplicate logic; stars need joins | Star as the source of truth; wide tables as derived, rebuildable outputs |
Grain: the challenge behind most wrong numbers
The grain is what one row means. If a table mixes order headers and order lines, or a join quietly changes one row per order into one row per order line, every sum after that is wrong. Most "the dashboard is wrong" incidents trace back to grain.
- Declare the grain in writing for every fact table and put it in the table comment
- Enforce it with a uniqueness test on the grain columns, such as dbt
uniqueonorder_line_id - Never store measures of two different grains in one fact; build two facts and combine them as the next section shows
History: joining to the right version
With SCD Type 2 a customer has one row per version. Joining facts to it on the business key alone matches every version, which duplicates rows. Join on the version valid on the event date, or store the version's surrogate key in the fact at load time.
-- dim_customer_scd2: C1 lived in Pune until 2026-06-30, then Mumbai SELECT o.order_id, d.customer_sk, d.city, o.amount FROM orders o JOIN dim_customer_scd2 d ON o.customer_id = d.customer_id AND o.order_date BETWEEN d.valid_from AND d.valid_to;
| Join | Rows returned (2 orders) | Result |
|---|---|---|
| On customer_id only | 4 | Every order appears once per city: wrong |
| On customer_id and date range | 2 | O1 in Pune (key 1), O2 in Mumbai (key 2): right |
Tested on PySpark 3.5. A date range join cannot use a plain equi-join alone, so on very large tables resolve the surrogate key once at load time and store it in the fact; queries then join on a simple key.
Late-arriving dimensions: inferred members
An order for customer C9 arrives before the CRM sends C9. An inner join drops the order; a left join leaves a NULL key. The standard fix is an inferred member: insert a placeholder row for C9 now, key the fact to it, and overwrite the placeholder's attributes when the real row arrives. The fact never needs to be touched again.
INSERT INTO dim_customer
SELECT (SELECT MAX(customer_sk) FROM dim_customer)
+ ROW_NUMBER() OVER (ORDER BY o.customer_id) AS customer_sk,
o.customer_id, 'Unknown (pending)' AS name, true AS is_inferred
FROM (SELECT DISTINCT customer_id FROM staged_orders) o
LEFT ANTI JOIN dim_customer d ON o.customer_id = d.customer_id;
Tested: C9 got key 3 with is_inferred = true and both orders loaded. In production, use an identity column or a hash key instead of MAX + ROW_NUMBER, which is not safe when two loads run at once.
Many-to-many: bridge tables
A joint bank account has two owners. Join the balance fact to customers directly and the balance is counted twice. A bridge table (account_id, customer_id, weight) sits between them; weights such as 0.5 and 0.5 allocate the measure so totals still add up, and queries that need "accounts per customer" can ignore the weight.
A checklist before you call a model done
- Grain written down and tested for uniqueness
- Every dimension conformed or deliberately local
- SCD type chosen per attribute, not per table
- Unknown and inferred members defined; no NULL foreign keys
- Late facts and deletes handled, with a reprocessing window
- Metrics defined once; totals reconciled to the source system
- PII tagged; partitioning and clustering chosen for the main queries
Interview-ready summary
"The hardest modelling problems are rarely the diagram. They are grain, keys, history and timing."
"I start by writing the grain of every fact as one sentence and testing it for uniqueness, because mixed grain is the main cause of double counting. I use surrogate keys and conformed dimensions so the same customer is one customer everywhere, with MDM when sources disagree."
"For history I choose SCD type per attribute and join facts to the version valid at the event time. For late-arriving dimensions I insert inferred members, so no fact is dropped. Many-to-many relationships go through bridge tables with weights."
"Finally, I make definitions shared: conformed dimensions, a semantic layer for metrics, data contracts with source teams, and reconciliation against source totals, so the model stays trusted as it grows."
14. Joining Two Fact Tables
A classic interview question: "What happens when you join two fact tables, given that facts are huge?" The short answer is that you usually get wrong numbers and a very expensive query at the same time. The good answer explains why, and what to do instead.
Why fact-to-fact joins go wrong
Two fact tables usually share only dimensions (customer, product, date), not a one-to-one key. Joining on a shared dimension is a many-to-many join: every sales row for a customer pairs with every return row for that customer. Kimball calls the result a fan trap (or chasm trap): rows multiply and measures are counted many times.
fact_sales (grain: order line) fact_returns (grain: return line)
customer line sales customer return amount
C1 L1 100 C1 R1 40
C1 L2 50 C1 R2 30
C1 L3 30
C2 L4 80
JOIN ON customer_id -> C1: 3 sales rows x 2 return rows = 6 rows
C2: no returns, so an inner join drops C2 entirely
SELECT s.customer_id,
COUNT(*) AS joined_rows,
SUM(s.sales_amount) AS total_sales,
SUM(r.return_amount) AS total_returns
FROM fact_sales s
JOIN fact_returns r ON s.customer_id = r.customer_id
GROUP BY s.customer_id;
| customer | joined_rows | total_sales | total_returns | Truth |
|---|---|---|---|---|
| C1 | 6 | 360 (doubled) | 210 (tripled) | Sales 180, returns 70 |
| C2 | missing | missing | missing | Sales 80, returns 0 |
Tested on PySpark 3.5. Each sales row is repeated once per matching return row and each return row once per sales row. No error is raised: the query looks fine and the numbers are silently wrong.
What happens to the engine at huge volume
The output size of a many-to-many join is the sum over every key of (rows on the left) × (rows on the right). With billions of rows that number becomes astronomical, and the engine has to do all the work below before it can return anything:
| Stage | What happens | Symptom |
|---|---|---|
| Shuffle | Neither fact fits in memory, so broadcast is impossible. Both sides are hash-partitioned on the join key and sent across the network | Terabytes of shuffle read and write |
| Sort-merge or hash join | Spark chooses a sort-merge join; warehouses use a distributed hash join. Each partition pairs every left row with every right row for the same key | Long stages, high CPU |
| Row explosion | Output rows = Σ (left rows × right rows) per key, often 100× or 1,000× the inputs | Snowflake Query Profile flags an "exploding join"; Spark shows output rows far above input rows |
| Skew | Hot keys, such as a marketplace seller or an "unknown" customer, land on one task | One task runs for hours while the others finish in seconds |
| Spill and failure | Partitions exceed memory and spill to disk | Disk spill, out-of-memory errors, executor loss, retries |
| Cost | Warehouse credits or cluster hours scale with the exploded rows, not the inputs | One dashboard query costs more than the nightly load |
A tested example at small scale: 300,000 sales rows and 30,000 return rows over 1,000 customers.
| Approach | Rows produced | Total sales reported |
|---|---|---|
| Naive join on customer_id | 9,000,000 (27× the 330,000 inputs) | 450,161,414: 30× the truth |
| Drill-across (below) | 1,000 | 15,005,380: correct |
Scale the same shape to 3 billion sales rows and the naive join would produce trillions of rows. That is why the query either runs for hours or fails, and why the answer would be wrong even if it finished.
Estimate the damage before you join
Counting the rows per key on each side tells you how big a join will be, using only cheap aggregations:
SELECT SUM(s.n * r.n) AS rows_the_join_will_produce,
MAX(s.n * r.n) AS rows_from_worst_key
FROM (SELECT customer_id, COUNT(*) AS n FROM fact_sales GROUP BY customer_id) s
JOIN (SELECT customer_id, COUNT(*) AS n FROM fact_returns GROUP BY customer_id) r
ON s.customer_id = r.customer_id;
Tested: it predicted 9,000,000 rows and a worst key of 9,000, matching the real join exactly. If the result is far above the larger input, the join is many-to-many.
The right patterns
1. Drill-across: aggregate first, then join
Bring each fact to the same grain on conformed dimensions, then join the small results. This is Kimball's drill-across, and it is what BI tools such as Power BI and Looker do behind the scenes when one report uses two facts through shared dimensions.
WITH s AS (SELECT customer_id, SUM(sales_amount) AS total_sales
FROM fact_sales GROUP BY customer_id),
r AS (SELECT customer_id, SUM(return_amount) AS total_returns
FROM fact_returns GROUP BY customer_id)
SELECT COALESCE(s.customer_id, r.customer_id) AS customer_id,
COALESCE(s.total_sales, 0) AS total_sales,
COALESCE(r.total_returns, 0) AS total_returns
FROM s FULL OUTER JOIN r ON s.customer_id = r.customer_id;
Result: C1 180 and 70, C2 80 and 0. Use a full outer join so customers that appear in only one fact are kept. The aggregations are cheap because engines aggregate partially before the shuffle, and the join is now one-to-one on far fewer rows.
2. Union, then aggregate (no join at all)
SELECT customer_id,
SUM(sales_amount) AS total_sales,
SUM(return_amount) AS total_returns
FROM (
SELECT customer_id, sales_amount, 0 AS return_amount FROM fact_sales
UNION ALL
SELECT customer_id, 0, return_amount FROM fact_returns
) t
GROUP BY customer_id;
Same correct result. Stacking the facts and grouping once avoids the join and its skew entirely, and scales well because a group-by can combine rows partially before the shuffle, unlike a join.
3. Join at the true shared grain
Sometimes two facts really are related row to row: a return points to the order line it reverses. Then join on that key (often a degenerate dimension such as order_line_id), after aggregating the side that can have several rows per key:
SELECT s.order_line_id, s.sales_amount,
COALESCE(r.returned, 0) AS returned,
s.sales_amount - COALESCE(r.returned, 0) AS net_amount
FROM fact_sales s
LEFT JOIN (SELECT order_line_id, SUM(return_amount) AS returned
FROM fact_returns GROUP BY order_line_id) r
ON s.order_line_id = r.order_line_id;
Tested: L1 100 − 40 = 60, L3 30 − 30 = 0, L2 and L4 unchanged. The join is now one-to-one, so no row multiplies.
4. Model it once: consolidated and accumulating facts
| Pattern | When to use it | Example |
|---|---|---|
| Consolidated fact | Two facts are always analysed together at one grain | fact_sales_and_returns_daily by customer, product, day |
| Accumulating snapshot | A process with milestones | One row per order with ordered, shipped, delivered and returned dates |
| Aggregate table or materialized view | Common dashboard queries | Daily revenue and returns per store, refreshed incrementally |
| Semantic layer metrics | Many tools need the same cross-fact metric | Net revenue = sales − returns, defined once |
When you really must join two large facts
| Technique | Why it helps |
|---|---|
| Filter and prune first | Restrict dates so partition and file pruning skip most data; select only needed columns |
| Pre-aggregate the many side | Turns many-to-many into one-to-many before the join |
| Co-locate on the join key | Liquid clustering or Z-order in Delta, clustering keys in Snowflake, clustering in BigQuery, bucketing or storage-partitioned joins in Iceberg and Hive tables reduce or remove the shuffle |
| Adaptive Query Execution | Spark 3.2+ enables AQE by default: it splits skewed partitions and switches to broadcast when a side turns out small after filtering |
| Salt hot keys | Spreads one huge key across many tasks (below) |
| Process incrementally | Join only new or changed partitions instead of the full history every run |
# Salting: spread a hot key across 16 tasks
from pyspark.sql import functions as F
SALT = 16
fact = big_fact.withColumn("salt", (F.rand() * SALT).cast("int"))
other = small_side.crossJoin(spark.range(SALT).withColumnRenamed("id", "salt"))
joined = fact.join(other, ["customer_id", "salt"])
Tested: a key holding 150,000 of 200,000 rows was split into 16 pieces of about 9,300 rows each, and the salted join returned exactly the same count and sum as the plain join. Salt the big side randomly and copy the other side once per salt value; it only makes sense when the other side is small enough to copy.
== physical plan of the naive join, with broadcast disabled == SortMergeJoin [customer_id], [customer_id], Inner :- Sort [customer_id ASC] : +- Exchange hashpartitioning(customer_id, 8) <- shuffle of fact 1 +- Sort [customer_id ASC] +- Exchange hashpartitioning(customer_id, 8) <- shuffle of fact 2
Simplified from the tested plan. Two Exchange nodes mean both facts cross the network; that is the cost you pay before a single row is matched.
Common mistakes
| Mistake | Effect | Fix |
|---|---|---|
| Joining facts on a shared dimension key | Fan-out, inflated totals | Drill-across or union then aggregate |
| Inner join between facts | Members missing from one fact disappear | Full outer join of the aggregates |
| Using COUNT(DISTINCT) or AVG to hide fan-out | Some measures look right, sums stay wrong | Fix the grain, not the symptom |
| Joining full history every run | Cost grows every day | Incremental processing and partition filters |
| Ignoring skew | One task runs for hours | AQE skew handling, salting, filter out unknown keys |
Interview-ready summary
"Two fact tables normally share only dimensions, so joining them on, say, customer_id is many-to-many. Every sales row pairs with every return row for that customer, so totals are multiplied. That is the fan trap. An inner join also drops customers that exist in only one fact."
"At big-data volume it is also a performance problem. Neither side can be broadcast, so both facts are shuffled on the join key and sort-merged. The output is the sum of left rows times right rows per key, so it can be hundreds of times the input. Hot keys cause skew, then spills, out-of-memory failures and high cost."
"So I avoid joining facts directly. I use drill-across: aggregate each fact to the same grain on conformed dimensions, then full outer join the small results. Or I union the facts and aggregate once. If the facts are truly related row to row, I join on that shared key, such as order_line_id, after aggregating the many side."
"If the business always needs them together, I model it once as a consolidated fact, an accumulating snapshot or an aggregate table, with the metric in the semantic layer. When a large join is unavoidable I filter early, cluster on the join key, rely on AQE for skew, salt hot keys and process incrementally."
15. Data Governance
Data governance is the set of roles, rules and processes that decide how data is owned, defined, protected and used. Tools enforce it, but governance starts with people: if nobody owns the customer table, no tool can decide who should see it.
The three parts of governance
| Part | What it covers | Retailer example |
|---|---|---|
| People | Owners, stewards and a governance council | The CRM team owns customer data |
| Process | How access is requested, how definitions are agreed, how issues are fixed | Access requests approved by the owner within 2 days |
| Technology | Catalog, access control, lineage, quality and audit tools | Unity Catalog, plus a business catalog |
Roles
| Role | Responsible for |
|---|---|
| Data owner | A senior business person accountable for a data domain; approves access and rules |
| Data steward | Day-to-day definitions, quality rules and metadata for that domain |
| Data custodian | The technical team that stores and secures the data |
| Data consumer | Uses data within the agreed rules |
| Governance council | Sets company-wide policies and settles disputes |
Operating models
| Model | How it works | Suits |
|---|---|---|
| Centralised | One team sets and enforces all rules | Smaller or highly regulated companies |
| Federated | A central team sets standards; domains apply them | Most large companies, and data mesh |
| Decentralised | Each domain decides alone | Rarely works: definitions drift |
Our retailer chooses the federated model. A small central team defines classification levels, naming rules and security standards. The Sales, CRM and Finance domains each own their data and apply those rules.
Policies a company needs
- Data classification policy: the sensitivity levels and how to label data
- Access policy: who approves access, and how often it is reviewed
- Retention policy: how long each kind of data is kept
- Data quality policy: required checks and who fixes failures
- Acceptable use policy: what data may be used for, including AI
Many companies follow DAMA-DMBOK, the Data Management Body of Knowledge, which puts data governance at the centre of ten other knowledge areas, including architecture, modelling, security, quality and metadata.
Governance tools
| Tool | Type | Strengths |
|---|---|---|
| Databricks Unity Catalog | Platform-native governance | Enforces access, captures lineage and audit automatically |
| Microsoft Purview | Enterprise catalog and compliance | Microsoft and Azure estates, classification |
| Collibra | Business-focused governance platform | Glossary, stewardship workflows, policy management |
| Alation | Data catalog | Search, collaboration, query-log based usage |
| Atlan | Modern active metadata platform | Wide connectors, collaboration |
| Informatica CDGC | Enterprise data governance and catalog | Large estates, data quality and MDM suite |
| DataHub, OpenMetadata | Open-source metadata platforms | Free, extensible, strong lineage |
| Apache Atlas | Open-source, Hadoop era | Hive and Hadoop ecosystems |
| AWS Lake Formation, Glue Data Catalog | AWS-native | Permissions and catalog for AWS data lakes |
| Google Dataplex | Google Cloud-native | Catalog and governance on Google Cloud |
A common pattern is two layers: the platform's own catalog, such as Unity Catalog, enforces rules where the data lives, and an enterprise catalog, such as Collibra or Purview, describes data across every system for business users.
16. Metadata and Catalogs
Governance needs metadata, meaning data about data. Without it, nobody can find a table, trust a definition or tell which column holds an email address.
| Kind | Examples | Who uses it |
|---|---|---|
| Technical | Schema, data types, location, size | Engineers |
| Business | Definitions, glossary terms, owners | Analysts, business users |
| Operational | Last refresh, row counts, job runs | Operations, support |
| Governance | Classification tags, access rules, retention | Security, compliance |
How Unity Catalog organises data
Metastore (one per region)
└─ Catalog prod_sales often one per domain or environment
└─ Schema gold
├─ Table fact_order_line
├─ View v_customer_masked
├─ Volume raw_files non-table files
├─ Function mask_email UDFs, masks, filters
└─ Model churn_model ML models
Full name: catalog.schema.object -> prod_sales.gold.fact_order_line
Each object can carry the metadata above: a comment for the business definition, an owner, and tags for classification.
COMMENT ON TABLE prod_sales.gold.dim_customer IS
'One row per version of a customer (SCD2). Owner: CRM team.';
ALTER TABLE prod_sales.gold.dim_customer OWNER TO `crm-owners`;
ALTER TABLE prod_sales.gold.dim_customer
ALTER COLUMN email SET TAGS ('pii' = 'email');
A business glossary
A glossary defines business terms once, such as Active Customer: placed an order in the last 90 days, and links each term to the tables and columns that implement it. This is what stops three dashboards from showing three different numbers for the same thing.
Catalogs on other platforms
| Platform | Technical catalog | Enterprise or business catalog |
|---|---|---|
| Databricks | Unity Catalog | Unity Catalog, or an external catalog |
| Snowflake | Snowflake Horizon (built in) | Horizon, or an external catalog |
| AWS | Glue Data Catalog | SageMaker Catalog (formerly DataZone) |
| Azure | Fabric and OneLake catalog | Microsoft Purview |
| Google Cloud | BigQuery metadata | Knowledge Catalog (formerly Dataplex) |
| Open source | Hive Metastore, Apache Polaris (Iceberg REST) | DataHub, OpenMetadata, Amundsen |
The architectural decision is which catalog is the source of truth for each kind of metadata. A common answer: each platform's own catalog enforces access, and one enterprise catalog collects metadata, glossary and lineage from all of them.
17. Data Classification
Before deciding how to protect data, you must know how sensitive it is. Classification gives every dataset, and ideally every column, a label that the rest of the platform can act on.
| Level | Meaning | Retailer example | Typical controls |
|---|---|---|---|
| Public | Safe to publish | Product catalogue | None special |
| Internal | For employees | Daily sales totals | Login required |
| Confidential | Limited to certain teams | Supplier prices, margins | Group-based access, audit |
| Restricted | Serious harm if leaked | Emails, phone numbers, payment data | Masking, encryption, strict approval |
Types of sensitive data
| Term | Meaning | Examples |
|---|---|---|
| PII | Personally identifiable information | Name, email, phone, address |
| Sensitive personal data | Higher-risk categories | Health, biometrics, religion, finances |
| PHI | Protected health information (US HIPAA) | Diagnoses, prescriptions |
| PCI data | Payment card data | Card number, CVV |
| Quasi-identifiers | Not identifying alone, but together they can be | Postcode + birth date + gender |
Classification only helps if it drives action. In Unity Catalog, a governed tag is a tag from an approved list, so it cannot be misspelled, and access policies can use it directly, as the security section shows. Databricks documents a safe default: tag every new object as unverified, block access until it is reviewed, then apply the right tags.
Finding sensitive data automatically
In a large estate you cannot tag every column by hand. Discovery tools scan data and suggest classifications, which stewards then confirm.
| Platform | Discovery tool |
|---|---|
| AWS | Amazon Macie (S3) |
| Azure and multi-cloud | Microsoft Purview |
| Google Cloud | Sensitive Data Protection |
| Snowflake | Built-in data classification |
| Databricks | Data classification in Unity Catalog |
| Multi-platform | BigID, Collibra, Informatica, Immuta, Privacera |
18. Data Privacy
Privacy is about using personal data fairly and lawfully: collecting only what you need, using it only for the stated purpose, and respecting people's rights. Security is about protecting data from unauthorised access. A perfectly secure system can still break privacy law, for example by keeping data for too long or using it for something nobody agreed to.
Privacy principles
| Principle | Meaning | Architecture decision |
|---|---|---|
| Purpose limitation | Use data only for the stated purpose | Tag tables with their allowed purpose |
| Data minimisation | Collect and keep only what is needed | Do not copy emails into gold if no report needs them |
| Consent | A lawful basis, often consent, for processing | Store consent and filter by it |
| Storage limitation | Keep data no longer than needed | Retention rules, then delete |
| Rights of individuals | Access, correction and erasure | Be able to find and delete one person's data |
| Accountability | Prove that you comply | Audit logs, lineage, documentation |
Laws and standards an architect meets
| Regulation | Where | Key points |
|---|---|---|
| DPDP Act 2023 | India | Consent, purpose limits and individual rights. The DPDP Rules were notified in November 2025, with most duties applying from May 2027. Penalties reach ₹250 crore |
| GDPR | EU | Right to erasure; breach notification within 72 hours; fines up to 4% of global turnover |
| CCPA / CPRA | California | Rights to know, delete, and opt out of sale or sharing |
| HIPAA | US healthcare | Protects health information |
| PCI DSS | Card payments worldwide | An industry standard, not a law; strict rules for card data |
This is a summary for architecture planning, not legal advice. Confirm exact obligations and dates with your legal team.
Privacy techniques
| Technique | What it does | Reversible? | Use for |
|---|---|---|---|
| Masking | Hides part of a value when shown | The data is unchanged underneath | Showing data to analysts |
| Pseudonymisation | Replaces an identifier with a code | Yes, with the key or mapping | Joining data without revealing who |
| Tokenisation | Swaps a value for a random token, kept in a vault | Yes, through the vault | Card numbers |
| Encryption | Scrambles data with a key | Yes, with the key | Storage and transfer |
| Aggregation | Keeps only totals | No | Reports |
| Anonymisation | Removes any way to identify people | No, if done properly | Sharing and research |
Tested: hashing is not anonymisation
A common mistake is to hash emails with SHA-256 and call the data anonymous. We hashed two emails, then hashed a list of guessed emails and joined the two:
from pyspark.sql import functions as F
hashed = customers.select(F.sha2("email", 256).alias("email_hash"), "spend")
guesses = candidate_emails.select(
"email", F.sha2("email", 256).alias("email_hash"))
hashed.join(guesses, "email_hash").select("email", "spend").show()
# Both customers were re-identified, with their spend.
Anyone with a list of likely emails can reverse a plain hash. A keyed hash, mixing in a secret held in a secret store, stopped the attack in our test: 0 matches. The same email still produced the same value, so joins kept working.
key = dbutils.secrets.get("privacy", "hash_key") # never in code
pseudo = F.sha2(F.concat(F.lit(key), F.col("email")), 256)
Under laws such as GDPR, keyed hashes are still personal data, called pseudonymised data, because whoever holds the key can link them back. They lower risk; they do not remove legal duties. A dedicated keyed function such as HMAC is the stronger form of this idea.
Tested: deleting a person from a Delta table
Erasure requests are harder in a lakehouse than they look, because Delta keeps old versions for time travel. We deleted customer C200 and checked what remained:
| Step | Result |
|---|---|
DELETE ... WHERE customer_id = 'C200' | The current table has 2 rows |
| Read version 0 with time travel | C200's email is still readable |
VACUUM ... RETAIN 0 HOURS | Refused by a safety check, unless it is turned off |
| VACUUM after the old files expire | The old data file is deleted |
| Read C200 from version 0 again | Fails: the data is gone |
count() on version 0 | Still says 3, from the log's statistics, not the files |
DESCRIBE HISTORY | Still shows the DELETE and VACUUM, which is your evidence |
So erasure is a process, not a single statement: delete from every table that holds the person, including bronze and any copies, then let VACUUM remove the old files after the retention period. Lineage tells you which tables to check.
DELETE FROM prod_sales.silver.customer WHERE customer_id = 'C200'; DELETE FROM prod_sales.gold.dim_customer WHERE customer_id = 'C200'; -- after the retention period (default 7 days), old files are removed VACUUM prod_sales.silver.customer; VACUUM prod_sales.gold.dim_customer;
The default retention exists to protect running readers and time travel; do not set it to zero in production. Check your retention settings against the deadline the law gives you. Raw files in bronze, exports and backups also need a deletion plan.
Erasure on other platforms
Every modern platform keeps deleted data for a while to protect against mistakes, so the Delta lesson applies everywhere: a DELETE is only the first step, and the erasure is complete once the recovery copies expire.
| Platform | Where deleted data survives | How it is finally removed |
|---|---|---|
| Delta Lake | Old versions for time travel | VACUUM after the retention period |
| Apache Iceberg | Old snapshots | Expire snapshots, then remove orphan files |
| Snowflake | Time Travel (up to 90 days on Enterprise), then 7 days of Fail-safe | Expires automatically; shorten Time Travel on sensitive tables |
| BigQuery | Time travel window (up to 7 days), then fail-safe | Expires automatically |
| S3, ADLS, GCS | Object versions and soft delete, if enabled | Lifecycle rules for old versions |
| Backups and exports | Snapshots, extracts, test copies | Your own retention schedule |
Document these windows in your privacy notice and erasure process, because "deleted" usually means "deleted from live use now, and from recovery copies within a known period".
Data residency and cross-border transfers
Many laws care about where personal data is stored and processed. GDPR restricts transfers outside the EU unless safeguards are in place, and India's DPDP Act lets the government restrict transfers to certain countries. For an architect this becomes concrete: choose cloud regions deliberately, keep EU customers' personal data in an EU region, and make sure backups, logs, support access and analytics copies follow the same rule. Aggregated or anonymised data can usually move more freely.
19. Data Security and How to Implement It
Security protects data from unauthorised access, change or loss. No single control is enough, so architects use defence in depth: several layers, so that one mistake does not expose everything.
| Layer | Question | Controls |
|---|---|---|
| Identity | Who are you? | Single sign-on, MFA, users and groups synced from the identity provider, service principals |
| Access | What may you do? | Grants to groups, least privilege, ABAC policies |
| Fine-grained | Which rows and columns? | Row filters, column masks, dynamic views |
| Encryption | Can stolen data be read? | At rest, in transit (TLS), customer-managed keys |
| Network | Where can you connect from? | Private connectivity, no public IPs, IP access lists |
| Secrets | Where are passwords kept? | A secret store, never in code |
| Monitoring | What happened? | Audit logs, alerts, regular access reviews |
Implementation steps on any platform
Whatever the vendor, security is implemented in the same order. The Databricks example below and the other platforms after it all follow these steps.
| # | Step | Done when |
|---|---|---|
| 1 | Federate identity from one identity provider (Entra ID, Okta, Ping) | No local accounts; MFA everywhere |
| 2 | Create role-based groups and service identities | People and jobs are in groups, not granted directly |
| 3 | Grant least privilege to groups | Every grant has an owner and a reason |
| 4 | Classify and tag sensitive data | Personal data columns are tagged |
| 5 | Apply masking and row-level rules, ideally by tag | Analysts cannot see raw personal data |
| 6 | Encrypt at rest and in transit, with managed keys where needed | Key ownership documented |
| 7 | Isolate the network | No public endpoints for data services |
| 8 | Move secrets to a vault | No passwords in code or config |
| 9 | Centralise audit logs and alerts | You can answer "who read this?" in minutes |
| 10 | Review access regularly | Quarterly certification by data owners |
This approach is often called zero trust: never trust a request because of where it comes from, verify every identity, and give the least access needed.
Example: implementing it on Databricks
Here is how the retailer answers the auditor's question, layer by layer.
1. Connect identity. Sync users and groups from the identity provider, such as Microsoft Entra ID or Okta, with SCIM. Create groups by role, for example analysts, crm-owners and data-engineers. Production jobs run as service principals, not as people.
2. Grant to groups, with least privilege. Each group gets only what its job needs. Because grants go to groups, moving a person between teams changes their access automatically.
GRANT USE CATALOG ON CATALOG prod_sales TO `analysts`; GRANT USE SCHEMA ON SCHEMA prod_sales.gold TO `analysts`; GRANT SELECT ON SCHEMA prod_sales.gold TO `analysts`; GRANT ALL PRIVILEGES ON SCHEMA prod_sales.silver TO `data-engineers`; SHOW GRANTS ON TABLE prod_sales.gold.dim_customer;
3. Protect sensitive columns. Analysts need customer counts by city, but not emails. A column mask hides the value for everyone outside an approved group, while the table stays the same.
CREATE FUNCTION prod_sales.security.mask_email(email STRING)
RETURNS STRING
RETURN CASE
WHEN is_account_group_member('crm-owners') THEN email
ELSE concat(left(email, 1), '***@', substring_index(email, '@', -1))
END;
ALTER TABLE prod_sales.gold.dim_customer
ALTER COLUMN email SET MASK prod_sales.security.mask_email;
The masking expression itself was tested in Spark: asha@example.com became a***@example.com, and a card number became ************1111.
4. Limit rows where needed. Regional teams should see only their region's orders. A row filter applies that automatically to every query.
CREATE FUNCTION prod_sales.security.region_filter(region STRING)
RETURNS BOOLEAN
RETURN is_account_group_member('sales-all-regions')
OR (region = 'IN' AND is_account_group_member('sales-india'))
OR (region = 'EU' AND is_account_group_member('sales-europe'));
ALTER TABLE prod_sales.gold.fact_sales
SET ROW FILTER prod_sales.security.region_filter ON (region);
5. Scale it with tags and ABAC. Writing a mask for each table does not scale to hundreds of tables. Attribute-based access control applies one policy to every column carrying a tag, including tables created later. Databricks now recommends ABAC for this, and it became generally available in April 2026.
-- tag the column once
ALTER TABLE prod_sales.gold.dim_customer
ALTER COLUMN email SET TAGS ('pii' = 'email');
-- one policy covers every email column in the schema
CREATE POLICY mask_emails
ON SCHEMA prod_sales.gold
COLUMN MASK prod_sales.security.mask_email
TO `account users`
FOR TABLES
MATCH COLUMNS has_tag_value('pii', 'email') AS email_col
ON COLUMN email_col;
ABAC needs Databricks Runtime 16.4 or above, or serverless compute, and some parts are still in Beta. Row filters and column masks have limits: for example, they cannot be applied to views, and time travel and clones are not supported on tables that use them.
6. Encrypt and isolate. Data is encrypted at rest and in transit. For stricter control, use customer-managed keys in the cloud key service, so that revoking the key makes the data unreadable. Use private connectivity so traffic does not cross the public internet.
7. Keep secrets out of code. Store passwords and keys in a secret scope, or in a cloud vault behind it, and read them at runtime.
password = dbutils.secrets.get(scope="etl", key="crm_db_password")
8. Audit and review. Every access is logged. Query the audit system table to answer the auditor directly, and review grants every quarter.
-- Who read the customer table in the last 30 days? SELECT event_time, user_identity.email, action_name FROM system.access.audit WHERE event_date >= current_date() - INTERVAL 30 DAYS AND request_params.full_name_arg = 'prod_sales.gold.dim_customer' ORDER BY event_time DESC;
The Databricks grants, masks, filters, policies and audit queries above follow the Databricks documentation; they need a Unity Catalog workspace and were not run here. Check audit column names in your workspace before relying on the query.
The same controls on other platforms
Here is the email mask and the regional row filter from the Databricks example, on the other major platforms. The pattern is the same everywhere: a policy object, attached to a column or table, deciding per user at query time.
Snowflake uses masking policies and row access policies, both requiring Enterprise Edition or higher. A masking policy can also be attached to a tag, so every tagged column is masked.
CREATE OR REPLACE MASKING POLICY email_mask AS (val STRING) RETURNS STRING ->
CASE
WHEN CURRENT_ROLE() IN ('CRM_OWNER') THEN val
ELSE '*********'
END;
ALTER TABLE customers MODIFY COLUMN email SET MASKING POLICY email_mask;
CREATE OR REPLACE ROW ACCESS POLICY region_policy
AS (region VARCHAR) RETURNS BOOLEAN ->
CURRENT_ROLE() = 'SALES_ALL'
OR (region = 'IN' AND CURRENT_ROLE() = 'SALES_INDIA');
ALTER TABLE sales ADD ROW ACCESS POLICY region_policy ON (region);
Google BigQuery uses row access policies that grant rows to principals, and policy tags for column-level security and data masking. Data masking is applied on top of row-level security.
CREATE ROW ACCESS POLICY india_rows
ON retail.sales
GRANT TO ("group:sales-india@example.com")
FILTER USING (region = "IN");
CREATE ROW ACCESS POLICY all_rows
ON retail.sales
GRANT TO ("group:sales-all@example.com")
FILTER USING (TRUE);
In BigQuery, creating the first row access policy hides all rows from anyone not granted, which is why the second policy gives full access back to the right group.
AWS Lake Formation controls access to data in S3 through the Glue Data Catalog. Data filters provide column, row and cell-level security when granting SELECT, and are enforced in Athena, Redshift Spectrum, EMR and Glue. LF-Tags give tag-based access control: a user whose granted tags match a resource's tags can access it.
Microsoft offers controls in several places. Fabric's OneLake security defines roles with row-level rules, written as SQL predicates, and column-level rules on Delta tables. Azure SQL and SQL Server provide dynamic data masking and row-level security:
-- SQL Server / Azure SQL: mask a column
ALTER TABLE dbo.Customers
ALTER COLUMN Email ADD MASKED WITH (FUNCTION = 'email()');
-- Row-level security: a predicate function plus a security policy
CREATE FUNCTION security.fn_region(@region NVARCHAR(10))
RETURNS TABLE WITH SCHEMABINDING AS
RETURN SELECT 1 AS allowed
WHERE IS_MEMBER('sales-all') = 1
OR (@region = 'IN' AND IS_MEMBER('sales-india') = 1);
CREATE SECURITY POLICY security.RegionFilter
ADD FILTER PREDICATE security.fn_region(Region) ON dbo.Sales
WITH (STATE = ON);
PostgreSQL has row-level security built in:
ALTER TABLE sales ENABLE ROW LEVEL SECURITY;
CREATE POLICY region_access ON sales
USING (region = current_setting('app.region'));
These examples follow each vendor's documentation and were not run here. Masking in SQL Server is a display-level control: users with ad hoc query access can sometimes infer values, so combine it with permissions.
Cross-platform security comparison
| Control | Databricks | Snowflake | BigQuery | AWS | Microsoft |
|---|---|---|---|---|---|
| Grants | Unity Catalog GRANT | Roles and GRANT | IAM roles | Lake Formation, IAM | Workspace roles, SQL GRANT |
| Column masks | Column masks | Masking policies | Policy tags, masking | Data filters | OneLake CLS, DDM |
| Row filters | Row filters | Row access policies | Row access policies | Data filters | OneLake RLS, SQL RLS |
| Tag-based policies | ABAC with governed tags | Tag-based masking | Policy tags | LF-Tags | Purview policies |
| Audit | system.access.audit | ACCESS_HISTORY | Cloud Audit Logs | CloudTrail | Purview audit, Monitor |
When data lives on several platforms, the risk is inconsistent rules: an email masked in Snowflake but visible in a copy on S3. Mitigations are to tag sensitive data consistently everywhere, generate policies from one definition (in code, or with a cross-platform tool such as Immuta or Privacera), and use lineage to find unprotected copies.
20. Data Lineage
Data lineage shows where data comes from, how it is transformed and where it goes. It answers the second half of the auditor's question: where does customer email data end up?
| Use | Question lineage answers |
|---|---|
| Impact analysis | If I change this column, which tables and dashboards break? |
| Root cause | This dashboard is wrong; which upstream job caused it? |
| Compliance | Where does personal data flow, and which copies must be erased? |
| Trust | Is this number built from approved sources? |
| Clean-up | Which tables does nothing read any more? |
Levels of lineage
| Level | Shows | Example |
|---|---|---|
| Job or process | Which job, notebook or task ran | load_fact_order_line ran at 06:00 |
| Dataset or table | Which tables feed which | silver.orders → gold.fact_order_line |
| Column | Which columns feed which | orders.amount → rpt_daily_sales.revenue |
How lineage is captured
| Method | How it works | Trade-off |
|---|---|---|
| Runtime capture | The engine reports what each query read and wrote | Accurate; needs support in each engine |
| Parsing SQL or code | A scanner reads queries and scripts | Covers many systems; misses dynamic code |
| Query logs | Reads the warehouse's query history | No code changes; only SQL |
| Manual registration | People add links by hand or API | Fills gaps; goes out of date |
Example: lineage in Databricks
Unity Catalog captures lineage automatically for queries run on Databricks through Spark DataFrames or Databricks SQL. Here is what happens when the retailer's daily job runs:
1. The job runs as usual. Nothing in the code mentions lineage.
spark.sql("""
CREATE OR REPLACE TABLE prod_sales.gold.rpt_daily_sales AS
SELECT f.order_date, c.city, SUM(f.net_amount) AS revenue
FROM prod_sales.gold.fact_order_line f
JOIN prod_sales.gold.dim_customer c ON f.customer_sk = c.customer_sk
GROUP BY f.order_date, c.city
""")
2. Unity Catalog records the links. From the query plan it learns that rpt_daily_sales reads fact_order_line and dim_customer, that revenue comes from net_amount, and which job or notebook ran it.
3. View it in Catalog Explorer. Open the table, choose the Lineage tab, then See Lineage Graph. Click a column to see its upstream and downstream columns, and filter for the jobs and dashboards that use the table.
4. Query it with SQL. Lineage is also in system tables, so it can drive reports and automation.
-- What reads dim_customer? SELECT DISTINCT target_table_full_name, entity_type FROM system.access.table_lineage WHERE source_table_full_name = 'prod_sales.gold.dim_customer' AND event_date >= current_date() - INTERVAL 90 DAYS; -- Where does the email column flow? SELECT DISTINCT target_table_full_name, target_column_name FROM system.access.column_lineage WHERE source_table_full_name = 'prod_sales.silver.customer' AND source_column_name = 'email';
| Unity Catalog lineage facts | |
|---|---|
| Captures | Tables, views, columns, notebooks, jobs, pipelines, dashboards, models |
| Scope | Across all workspaces on one metastore |
| Retention | Catalog Explorer keeps it indefinitely (data since September 2024); system tables keep 395 days by default |
| Does not capture | RDDs, global temp views, renamed objects, column lineage for path-based tables, column mappings inside UDFs |
| External systems | Added manually or by API as external metadata, such as a Kafka topic or a Tableau report |
To get full lineage, register tables in Unity Catalog and refer to them by name, not by path, and avoid RDDs. These rules are worth writing into your coding standards.
Example: lineage outside Databricks with OpenLineage
OpenLineage is an open standard for lineage events. A Spark listener sends an event when a job starts and finishes, listing the datasets it read and wrote. Marquez is its reference server, and DataHub, Purview and others can receive the same events.
spark = (SparkSession.builder
.config("spark.jars.packages",
"io.openlineage:openlineage-spark_2.12:1.40.1")
.config("spark.extraListeners",
"io.openlineage.spark.agent.OpenLineageSparkListener")
.config("spark.openlineage.transport.type", "http")
.config("spark.openlineage.transport.url", "http://marquez:5000")
.config("spark.openlineage.namespace", "retail")
.getOrCreate())
We ran this on local Spark 3.5, sending events to a file instead of a server. The result was a useful lesson:
| Operation | What OpenLineage recorded |
|---|---|
| Writing the raw orders and customers files | Each output dataset, with its schema |
| Joining them and writing a Delta table | Both inputs, but no output dataset |
| Joining them and writing Parquet | No lineage event at all |
So lineage coverage depends on the engine version, the command and the table format, and no tool catches everything. Test lineage capture for your real job types before promising an auditor complete coverage. We saw the same on Databricks' list of limitations above.
Lineage on other platforms
| Platform | Native lineage | Notes |
|---|---|---|
| Databricks | Unity Catalog, automatic | System tables keep 395 days |
| Snowflake | ACCESS_HISTORY view, plus a lineage graph in Snowsight | Column lineage needs Enterprise Edition; 365 days of history |
| Google Cloud | Knowledge Catalog data lineage | Automatic for BigQuery jobs; kept 30 days; accepts OpenLineage events |
| Azure | Microsoft Purview | Scans many sources; Databricks via Unity Catalog scans |
| AWS | SageMaker Catalog and partner tools | Often filled in with OpenLineage-based tools |
| Open-source Spark | OpenLineage listener, Spline | Sends events to Marquez, DataHub or others |
| dbt | Built-in model graph | Model-level; dbt platform adds column-level |
In Snowflake, column lineage is recorded in the objects_modified field of ACCESS_HISTORY, and can be flattened into source-to-target pairs:
SELECT ah.query_start_time,
om.value:"objectName"::STRING AS target_table,
col.value:"columnName"::STRING AS target_column,
src.value:"objectName"::STRING AS source_table,
src.value:"columnName"::STRING AS source_column
FROM snowflake.account_usage.access_history ah,
LATERAL FLATTEN(ah.objects_modified) om,
LATERAL FLATTEN(om.value:"columns") col,
LATERAL FLATTEN(col.value:"directSources") src
WHERE ah.query_start_time >= DATEADD(day, -30, CURRENT_TIMESTAMP());
The Snowflake query follows the structure in Snowflake's documentation and was not run here. Retention differs widely between platforms, from 30 days to over a year, so copy lineage into your own store if audits need longer history.
Other lineage tools
| Tool | How it gets lineage |
|---|---|
| dbt | Builds a model-level graph from ref() and source(), shown in dbt docs |
| Microsoft Purview | Scans sources; Unity Catalog lineage through scans, and Spark through OpenLineage |
| Collibra | Technical lineage scanners, including one for Unity Catalog |
| DataHub, OpenMetadata | Connectors for databases, Databricks, Kafka and Airflow, plus a Spark listener |
| Marquez | Reference store for OpenLineage events |
| Apache Atlas | Hooks for Hive and other Hadoop tools |
| Airflow | The OpenLineage provider reports what each task reads and writes |
21. Building Enterprise Lineage Across Systems
Unity Catalog covers what runs on Databricks. A real company also has source databases, APIs, Kafka, other Spark clusters and Airflow, so the architect has to join them into one picture. These seven steps build on each other.
Source DBs APIs Kafka Spark jobs Airflow DAGs
│ │ │ │ │
scanners manual schema reg. OpenLineage OpenLineage
(JDBC) / API + topics listener provider
└──────────┴──────────┴──────┬───────┴────────────────┘
▼
CENTRAL METADATA PLATFORM
(Collibra, DataHub, Atlas, Purview)
technical metadata + glossary + owners + lineage graph
│
impact analysis • visualisation • audit
1. Collect metadata from every source. Each kind of system needs a different collector:
| Source | How to collect | What you get |
|---|---|---|
| Source databases | Catalog scanners connect over JDBC and read the system catalog | Tables, columns, types, keys |
| APIs | Register the API and its schema (for example OpenAPI) as a dataset, by hand or API | Endpoints and fields |
| Kafka | Read topics and the schema registry | Topics, message schemas |
| Spark | The OpenLineage listener, or Unity Catalog on Databricks | Runtime inputs and outputs |
| Airflow | The OpenLineage provider (Airflow 2.7+) | Task runs and the datasets they touch |
2. Capture all three levels. Job-level lineage says which run produced the data. Dataset-level lineage links tables. Column-level lineage links fields, which is what you need to trace personal data and judge the impact of a column change.
3. Use OpenLineage where it fits. Choose the open standard for anything you control, such as Spark and Airflow, so that you are not tied to one vendor. Use native connectors where they are richer, such as Unity Catalog inside Databricks. Keep dataset names consistent: OpenLineage identifies a dataset by namespace and name, so the same table must get the same name from every tool, or the graph splits.
4. Store it centrally. Send everything to one metadata platform. Collibra is strongest for business governance and stewardship workflows; DataHub is open source with strong lineage and many connectors; Apache Atlas suits Hadoop-based estates. Purview is the natural choice in Microsoft-heavy companies.
5. Connect technical and business metadata. Link each table and column to a glossary term and an owner. Then the lineage graph can say "the Revenue KPI comes from CRM and Orders data, owned by the Sales domain", not only "table A feeds table B".
6. Build impact analysis. Walk the graph upstream to find causes and downstream to find what breaks. Run it automatically: when a pull request changes a column, list the affected tables, dashboards and owners, and notify them before merging.
7. Provide visualisation and audit. Users explore the graph in the catalog. Auditors get evidence: lineage for personal data, access logs, and the history of who changed what. Keep lineage history for as long as your audit period requires.
The hard part is rarely the tools. It is coverage, naming and ownership, so measure them: the percentage of tables with an owner, and the percentage of pipelines sending lineage.
Lineage interview questions with answers
Q1. What is the difference between data lineage and data provenance?
Lineage is the map of how data flows: which sources, jobs and transformations lead to a dataset, and where it goes next. Provenance is the origin and history of specific data: where a record came from, when, from which source version, and every change made to it, often used as evidence. Lineage says "revenue comes from orders through this job"; provenance says "this row came from file orders_0612.csv, loaded at 06:02 by run 8841, and was corrected on 14 June". Delta table history and audit logs provide provenance; lineage graphs provide lineage.
Q2. What is column-level lineage?
It links each column to the source columns and transformation that produce it. For example, rpt_daily_sales.revenue comes from SUM(fact_order_line.net_amount), which comes from orders.quantity × orders.unit_price. It is needed to trace personal data such as email through every copy, to judge the impact of changing one column, and to explain a metric. Tools build it from the query plan or by parsing SQL. It breaks down inside UDFs and path-based tables, which hide the column mapping.
Q3. How do you capture lineage across 100 Spark pipelines?
Never instrument each pipeline by hand; capture lineage at the platform level so new pipelines are covered automatically. On Databricks, Unity Catalog does this, provided every pipeline uses Unity Catalog tables by name, avoids RDDs, and runs on supported compute. Elsewhere, add the OpenLineage listener once, in the cluster defaults, an init script or a shared job template, with one namespace convention. Then enforce it: a CI check rejects path-based writes and missing table registration, and a coverage report lists pipelines that sent no lineage in the last week. Finally, test each job type: in our test, OpenLineage recorded a Delta write's inputs but not its output.
Q4. How do you capture lineage from Kafka to Delta Lake?
A Structured Streaming job reads the topic and writes a bronze Delta table. On Databricks, Unity Catalog records the Delta tables, but a Kafka topic is not a Unity Catalog object, so register it as external metadata (system type Kafka, entity type Topic) and link it to the bronze table with the External Lineage API, including column mappings. That external lineage shows in Catalog Explorer, but not in the lineage system tables. With OpenLineage, the Spark listener reports the Kafka topic as an input dataset, provided your version supports the source. Also collect topic schemas from the schema registry so the columns can be matched.
Q5. How do you identify downstream impact when a source column changes?
Use column-level lineage to walk downstream from the column, then expand to the jobs, dashboards and owners attached to each result. On Databricks, query system.access.column_lineage repeatedly, starting from the changed column, and join to table_lineage for jobs and dashboards. In dbt, dbt ls --select model+ lists downstream models. Automate it in CI so a pull request that drops or renames a column shows the impact list and notifies owners, and use data contracts so breaking changes need agreement first.
# Walk downstream from one column, level by level
frontier = {("prod_sales.silver.customer", "email")}
seen = set()
while frontier:
pairs = " OR ".join(
f"(source_table_full_name = '{t}' AND source_column_name = '{c}')"
for t, c in frontier)
rows = spark.sql(f"""
SELECT DISTINCT target_table_full_name AS t, target_column_name AS c
FROM system.access.column_lineage
WHERE ({pairs}) AND target_table_full_name IS NOT NULL""").collect()
frontier = {(r.t, r.c) for r in rows} - seen
seen |= frontier
print(sorted(seen)) # every downstream column that carries email
Q6. What is the difference between technical lineage and business lineage?
Technical lineage is system-level detail: tables, columns, jobs and SQL, for engineers. Business lineage is a simplified view in business terms: "the Monthly Revenue KPI is calculated from paid orders in the Order system, owned by Finance", for analysts, auditors and managers. Business lineage is built by linking technical assets to glossary terms and owners, and hiding intermediate steps.
Q7. How does Unity Catalog capture lineage?
Automatically, at runtime. When a query runs on Databricks compute through Spark DataFrames or Databricks SQL, Unity Catalog records which tables and columns were read and written, and which notebook, job, pipeline or dashboard ran it. It aggregates this across all workspaces on the metastore. You see it in Catalog Explorer and query it in system.access.table_lineage and column_lineage, kept 395 days by default. Tables must be registered in Unity Catalog. RDDs, global temp views, renamed objects and path-based column lineage are not captured, and external systems are added through external metadata.
Q8. What is OpenLineage, and how does it integrate with Airflow?
OpenLineage is an open standard for lineage events. Each event describes a run of a job with its input and output datasets, plus extra details called facets, such as schemas or column lineage. Airflow 2.7 and later integrate through the apache-airflow-providers-openlineage package. A listener fires when tasks start, complete or fail, and gets lineage from the operator in this order: a custom extractor, the operator's own OpenLineage methods, hook-level lineage, then manual inlets and outlets. You configure a transport, such as HTTP to Marquez or DataHub, or the console for testing. Where the operator supports it, Airflow passes parent run details to Spark, so a task links to the Spark job it launched.
Q9. How would you integrate Collibra with Databricks?
Collibra connects to Unity Catalog through an Edge site or a Collibra Cloud site. You create a Databricks Workspace connection, add the Technical Lineage for Databricks Unity Catalog capability, and synchronise manually or on a schedule, then review the ingestion report. It brings in databases, schemas, tables and columns with their lineage, with volumes and notebooks in preview, but not workflows. Use a service principal with only USE CATALOG, USE SCHEMA and SELECT; Collibra recommends a dedicated catalog of lineage views rather than direct access to query history tables. Then link the ingested assets to Collibra's glossary terms, owners and policies, so that business users see governed data with lineage.
Q10. How would you maintain lineage across multiple cloud platforms?
Choose one central metadata platform, such as DataHub, Collibra or Purview, and one standard, OpenLineage, for custom pipelines. Harvest native lineage from each platform: Unity Catalog per metastore, which means per region, AWS Glue, and Microsoft Fabric or Purview. Use a single naming convention for datasets, because the graph only joins up when the same dataset has the same identity everywhere. Register shared data, such as Delta Sharing recipients or cross-cloud copies, as explicit links. Run the collectors on a schedule, measure coverage per platform, and keep ownership and glossary terms in the central catalog, so the business view stays the same whichever cloud the data is in.
22. Data Quality
Governed, secure, traceable data is still useless if it is wrong. Data quality is measured on a few standard dimensions:
| Dimension | Question | Example check |
|---|---|---|
| Completeness | Is anything missing? | customer_id is never NULL |
| Validity | Does it follow the rules? | Email matches a pattern; quantity > 0 |
| Uniqueness | Are there duplicates? | One row per order line |
| Consistency | Does it agree across systems? | Fact total equals report total |
| Accuracy | Does it match reality? | Stock matches the warehouse count |
| Timeliness | Is it fresh enough? | Loaded before 7 a.m. |
Where to check, and with what
| Where | Tool | Example |
|---|---|---|
| In Databricks pipelines | Lakeflow Declarative Pipelines expectations | Drop rows where amount is negative |
| In dbt | Data tests and contracts | unique, not_null, relationships |
| In any Spark job | Great Expectations, Soda, Deequ | A suite of checks after each load |
| On tables over time | Lakehouse Monitoring | Drift and freshness alerts |
| In the table itself | Delta NOT NULL and CHECK constraints | Reject bad writes |
-- Lakeflow Declarative Pipelines: rules live with the table
CREATE OR REFRESH STREAMING TABLE silver_orders (
CONSTRAINT valid_amount EXPECT (amount > 0) ON VIOLATION DROP ROW,
CONSTRAINT has_customer EXPECT (customer_id IS NOT NULL)
ON VIOLATION FAIL UPDATE
) AS SELECT * FROM STREAM(bronze_orders);
Data contracts
A data contract is an agreement between the producer and consumers of a dataset: its schema, meaning, quality rules, freshness and owner. Breaking changes need notice. The dbt card shows one in practice: a model contract stops a build when a column's type changes.
Master data management
Customers often exist in several systems under different IDs, such as the CRM, the website and the loyalty app. Master data management (MDM) matches those records and creates one trusted golden record per customer, product or supplier. Tools include Informatica MDM, Reltio and Profisee, or matching rules built in Spark.
23. Data Quality KPIs at Enterprise Level
Checks catch bad rows; KPIs tell leadership whether data quality is improving across the whole company. A good enterprise scorecard measures three things: the data itself, the reliability of delivery, and how well the organisation responds when something breaks.
1. KPIs on the data (quality dimensions)
| KPI | Formula | Typical target for critical data |
|---|---|---|
| Completeness rate | Non-null required values ÷ required values | ≥ 99.5% |
| Validity rate | Values passing format and range rules ÷ values checked | ≥ 99% |
| Uniqueness rate | 1 − duplicate rows on the grain ÷ total rows | 100% on primary keys |
| Consistency rate | Records agreeing across systems ÷ records compared | ≥ 99% |
| Accuracy rate | Records matching a trusted source or audit sample ÷ records sampled | ≥ 98% |
| Referential integrity | Facts with a valid dimension key ÷ facts | 100%; unknown members tracked separately |
| Reconciliation variance | |platform total − source total| ÷ source total | < 0.1% for financial data |
2. KPIs on delivery (reliability)
| KPI | Formula | Why it matters |
|---|---|---|
| Freshness SLA attainment | Loads landed before the SLA time ÷ scheduled loads | Late data is wrong data for a 7 a.m. dashboard |
| Pipeline success rate | Successful runs ÷ total runs | Shows platform stability |
| Data downtime | Σ (time to detect + time to resolve) per incident | One number for how long data was wrong or missing |
| MTTD | Average time from the issue starting to being detected | Measures monitoring coverage |
| MTTR | Average time from detection to resolution | Measures response and ownership |
| Schema-change breakages | Unannounced breaking changes per month | Measures how well data contracts work |
3. KPIs on the programme (governance and impact)
| KPI | Formula | What it shows |
|---|---|---|
| Critical data element coverage | CDEs with active quality rules ÷ all CDEs | Whether the important data is actually monitored |
| Ownership coverage | Certified datasets with a named owner and steward ÷ certified datasets | Accountability |
| Issues found by consumers | Incidents reported by users ÷ all incidents | Lower is better: you find problems before the business does |
| Issue backlog and ageing | Open DQ issues, and how many are older than 30 days | Whether problems get fixed, not just logged |
| Certified dataset usage | Queries on certified (gold) data ÷ all analytical queries | Trust and adoption |
| Cost of poor quality | Rework hours, failed reports, regulatory findings, revenue lost | The business case, in money |
Rolling checks up into a score
Store every check result in one table, then roll it up by domain, table and dimension. Weight rules by severity so one failed critical rule counts more than many failed optional ones.
SELECT domain,
ROUND(100 * SUM(rows_passed) / SUM(rows_checked), 2) AS row_pass_pct,
ROUND(100 * SUM(w * rows_passed / rows_checked) / SUM(w), 2) AS dq_score
FROM (SELECT *, CASE severity WHEN 'critical' THEN 4 WHEN 'high' THEN 3
WHEN 'medium' THEN 2 ELSE 1 END AS w
FROM dq_check_results WHERE run_date = '2026-11-07') t
GROUP BY domain;
| domain | row_pass_pct | dq_score | Reading |
|---|---|---|---|
| crm | 85.00 | 88.33 | Optional phone numbers drag it down; the critical rules are not the problem |
| sales | 99.93 | 99.94 | Healthy: all critical rules near 100% |
SELECT COUNT(*) AS incidents,
ROUND(AVG((unix_timestamp(detected_at, 'yyyy-MM-dd HH:mm')
- unix_timestamp(started_at, 'yyyy-MM-dd HH:mm')) / 3600), 2) AS mttd_hours,
ROUND(AVG((unix_timestamp(resolved_at, 'yyyy-MM-dd HH:mm')
- unix_timestamp(detected_at, 'yyyy-MM-dd HH:mm')) / 3600), 2) AS mttr_hours,
ROUND(SUM((unix_timestamp(resolved_at, 'yyyy-MM-dd HH:mm')
- unix_timestamp(started_at, 'yyyy-MM-dd HH:mm')) / 3600), 2) AS data_downtime_hours,
ROUND(100 * AVG(CASE WHEN found_by = 'consumer' THEN 1 ELSE 0 END), 0) AS pct_found_by_consumers
FROM dq_incidents;
Tested on PySpark 3.5 with two incidents: MTTD 1.75 h, MTTR 3 h, data downtime 9.5 h, 50% found by consumers. A freshness query over the same sample gave 50% SLA attainment for gold.fact_sales (one late load out of two).
Making KPIs work in practice
- Start with critical data elements: the 50 to 200 fields behind revenue, regulatory and customer reporting, not every column
- Set targets per tier: gold and regulatory data strict, exploratory data relaxed
- Publish one scorecard per domain, with an owner for each red number
- Show trends, not snapshots: a score moving from 92% to 97% tells a better story than 97% alone
- Collect results automatically from Lakeflow expectations, dbt tests, Great Expectations, Soda or Lakehouse Monitoring into one results table
- Tie KPIs to business impact in the language of the business: "the finance close was late twice because of data", not "null rate 0.4%"
Interview-ready summary
"At enterprise level I measure data quality on three layers. First, the data: completeness, validity, uniqueness, consistency, accuracy, referential integrity and reconciliation to source, with targets per tier."
"Second, delivery reliability: freshness SLA attainment, pipeline success rate, MTTD, MTTR and data downtime. Data that is correct but late is still a failure for the business."
"Third, the programme: coverage of critical data elements, named owners, the share of issues found by consumers rather than by us, backlog ageing, and the cost of poor quality."
"I collect every check result in one table, weight rules by severity, roll scores up by domain and publish a trending scorecard with an owner for every red number. That turns quality from a list of failed tests into something leadership can manage."
24. Master Data Management (MDM)
Master data is the small set of core business entities that many systems share: customers, products, suppliers, employees, locations and accounts. Master data management (MDM) is the discipline, process and tooling that creates one trusted version of each of those entities, the golden record, and keeps every system linked to it.
The problem it solves: the same customer exists in the CRM as "Asha Rao", in the web shop as "asha rao" with another city, and in the loyalty app as "A. Rao" with only a phone number. Without MDM, reports count three customers, marketing sends three emails and compliance cannot answer "what do we hold about this person?"
Three kinds of data
| Type | What it is | Examples | Changes | Managed by |
|---|---|---|---|---|
| Master data | Core entities the business runs on | Customer, product, supplier, employee | Slowly | MDM |
| Reference data | Fixed code lists that classify other data | Country, currency, status codes, units | Rarely | Reference data management, often part of MDM |
| Transactional data | Events that happen to master data | Orders, payments, clicks, shipments | Constantly, high volume | OLTP applications, then the warehouse |
MDM vs a regular database vs a data warehouse
| Operational database (OLTP) | Data warehouse / lakehouse | MDM hub | |
|---|---|---|---|
| Main purpose | Run one application: take orders, store accounts | Analyse history across the business | Create and govern one trusted version of each core entity |
| Data held | That application's own records, including transactions | Integrated history of transactions and dimensions | Master entities only, plus links to every source record |
| Scope | One system | Many sources, read-only copies | Many sources, and it can feed corrections back |
| Duplicates | Unaware of the same customer in other systems | Inherits duplicates unless someone resolves them | Matches and merges duplicates: its core job |
| Key output | Rows for the app | Facts, dimensions, reports, ML features | Golden record, cross-reference (source ID → master ID), hierarchies |
| Data flow | Users and apps write to it | One way: sources → warehouse → BI | Two way: sources → hub → back to sources and to the warehouse |
| History | Usually current state only | Full history (SCD, snapshots) | Current golden record plus an audit trail of every merge and edit |
| Volume | High transaction rate | Very large: billions of rows | Small: millions of entities, not billions of events |
| People | App users | Analysts, data scientists | Data stewards who review matches and approve changes |
| Examples | PostgreSQL, Oracle, SQL Server behind an app | Snowflake, Databricks, BigQuery, Fabric | Informatica MDM, Reltio, SAP MDG, Profisee, Stibo STEP, Semarchy |
The key point: a database stores data for one application and a warehouse analyses data from many, but neither decides which record is the real customer. MDM is the system that decides, and both of the others then use its answer. In the warehouse, the MDM master ID becomes the business key of the conformed dim_customer.
CRM Web shop Loyalty app ERP
| | | |
+------------+------+------+-------------+
v
MDM HUB (match -> merge -> survivorship -> steward review)
golden records + cross-reference + hierarchies
|
+-------------+--------------+
v v
back to source apps data warehouse / lakehouse
(corrected address, dim_customer keyed by master_id,
master ID stamped) so every fact joins to one customer
How MDM works: the core steps
| Step | What happens | Example |
|---|---|---|
| 1. Standardise | Clean and normalise values before comparing them | Lower-case and trim emails; keep the last 10 digits of phones; parse addresses |
| 2. Match | Find records that are the same entity, using exact (deterministic) and fuzzy (probabilistic or ML) rules | Same email, or same phone, or similar name in the same city |
| 3. Merge | Group matched records under one master ID | CRM-1, WEB-7 and LOY-3 become master CRM-1 |
| 4. Survivorship | Choose the best value of each attribute from the group | Name from the most trusted source; email from the most recent update |
| 5. Steward review | People resolve uncertain matches and conflicts | "Ravi Kumar" vs "Ravi Kumarr": same person? |
| 6. Publish and sync | Share golden records and the cross-reference with every consumer | APIs or events to apps; tables to the warehouse |
A tested match, merge and survivorship example
Five customer records from three systems. Step 1 standardises the values and gives each source a trust rank:
CREATE OR REPLACE TEMP VIEW std AS
SELECT *,
NULLIF(LOWER(TRIM(email)), '') AS email_n,
RIGHT(REGEXP_REPLACE(phone, '[^0-9]', ''), 10) AS phone_n,
CASE source WHEN 'CRM' THEN 1 WHEN 'LOY' THEN 2 ELSE 3 END AS trust
FROM customer_sources;
Steps 2 and 3 match on email, then on phone, so records linked through either one end up under the same master ID:
CREATE OR REPLACE TEMP VIEW matched AS WITH pass1 AS ( SELECT *, MIN(source_id) OVER (PARTITION BY COALESCE(email_n, source_id)) AS g1 FROM std), pass2 AS ( SELECT *, MIN(g1) OVER (PARTITION BY COALESCE(phone_n, source_id)) AS g2 FROM pass1) SELECT *, MIN(g2) OVER (PARTITION BY g1) AS master_id FROM pass2;
| source_id | email (standardised) | phone (standardised) | master_id |
|---|---|---|---|
| CRM-1 | asha.rao@mail.com | 9845012345 | CRM-1 |
| WEB-7 | asha.rao@mail.com (was " ASHA.RAO@MAIL.COM ") | — | CRM-1 (same email) |
| LOY-3 | — | 9845012345 (was "98450-12345") | CRM-1 (same phone) |
| CRM-2 | ravi@mail.com | 9000000001 | CRM-2 |
| WEB-9 | ravi.k@other.com | 9000000002 | WEB-9 |
Step 4 builds the golden record with survivorship rules, one per attribute:
SELECT master_id,
MIN_BY(name, trust) AS name,
MAX_BY(email_n, CASE WHEN email_n IS NOT NULL THEN updated_at END) AS email,
MIN_BY(phone_n, CASE WHEN phone_n IS NOT NULL THEN trust END) AS phone,
MAX_BY(city, updated_at) AS city,
COUNT(*) AS source_records
FROM matched
GROUP BY master_id;
| master_id | name | phone | city | source_records | |
|---|---|---|---|---|---|
| CRM-1 | Asha Rao (CRM is most trusted) | asha.rao@mail.com | 9845012345 | Mumbai (latest update) | 3 |
| CRM-2 | Ravi Kumar | ravi@mail.com | 9000000001 | Delhi | 1 |
| WEB-9 | Ravi Kumarr | ravi.k@other.com | 9000000002 | Delhi | 1 |
Step 5 sends uncertain pairs to a data steward instead of merging them automatically:
SELECT a.master_id AS record_a, b.master_id AS record_b, a.name AS name_a, b.name AS name_b,
LEVENSHTEIN(LOWER(a.name), LOWER(b.name)) AS name_distance
FROM matched a JOIN matched b
ON a.master_id < b.master_id AND a.city = b.city
WHERE LEVENSHTEIN(LOWER(a.name), LOWER(b.name)) <= 2;
Tested on PySpark 3.5: five source records became three golden records, and one pair (CRM-2 and WEB-9, "Ravi Kumar" vs "Ravi Kumarr", distance 1) went to the review queue. The two-pass match is a teaching simplification: real MDM tools and libraries such as Splink or Zingg use blocking, weighted probabilistic scores and graph connected components, so chains of links of any length are merged.
The cross-reference table
MDM never throws the source IDs away. A cross-reference (xref) table maps every source record to its master ID, which is what lets the warehouse, the apps and auditors trace any golden record back to where it came from:
| master_id | source | source_id |
|---|---|---|
| CRM-1 | CRM | CRM-1 |
| CRM-1 | LOY | LOY-3 |
| CRM-1 | WEB | WEB-7 |
| CRM-2 | CRM | CRM-2 |
| WEB-9 | WEB | WEB-9 |
MDM implementation styles
| Style | How it works | Golden record lives | When to use it |
|---|---|---|---|
| Registry | Hub stores only match links and IDs; data stays in the sources | Assembled on read | Quick start, read-only analytics, least disruption |
| Consolidation | Hub copies and merges source data; sources are not updated | In the hub, for analytics | Better reporting and a single customer view in the warehouse |
| Coexistence | Hub merges data and syncs corrections back to sources | In the hub and the sources | Several systems keep editing, but should agree |
| Centralised (transaction hub) | Master data is created and edited only in the hub | In the hub only | Strict control, such as product or supplier onboarding |
Tools
| Category | Examples |
|---|---|
| Enterprise MDM platforms | Informatica MDM (IDMC), Reltio, SAP Master Data Governance, Profisee (integrates with Microsoft Purview and Fabric), Stibo Systems STEP, Semarchy, TIBCO EBX |
| Customer data platforms with identity resolution | Segment, mParticle, Salesforce Data Cloud: identity stitching for marketing, lighter on governance |
| Open-source entity resolution on Spark | Splink (probabilistic record linkage), Zingg (ML-based matching), often run on Databricks |
| Cloud services | AWS Entity Resolution |
Common challenges
| Challenge | How to handle it |
|---|---|
| Over-matching: two people merged into one | Conservative automatic thresholds, steward review for the grey zone, and an unmerge capability |
| Under-matching: duplicates remain | Better standardisation, more match attributes, fuzzy and ML matching |
| No business ownership | Named data owners and stewards per domain; MDM is a governance programme, not just a tool |
| Trying to master everything at once | Start with one domain, usually customer or product, and a few critical attributes |
| Privacy | Golden records concentrate personal data: classify, mask and apply consent and deletion across the xref |
| Keeping sources in sync | Event-driven updates and stamping the master ID back into source systems |
Types of MDM
"Types of MDM" can mean four different things, so a strong answer names the angle first. The two most often asked are by domain and by usage.
| Classified by | Types | What it means |
|---|---|---|
| Domain (which entity is mastered) | Customer or party, product, supplier or vendor, employee, location or site, asset, financial (chart of accounts, cost centres), reference data | Single-domain MDM masters one entity; multidomain MDM masters several on one platform and links them, such as customer ↔ product ↔ location |
| Usage | Operational, analytical, collaborative | Operational feeds golden records to live applications in real time; analytical feeds the warehouse and BI; collaborative runs workflows where several teams create and approve master data, such as new product or supplier onboarding |
| Implementation style | Registry, consolidation, coexistence, centralised | Where the golden record lives and whether sources are updated (table above) |
| Deployment | On-premises, cloud SaaS, hybrid | Reltio and Informatica IDMC are cloud-native; SAP MDG often runs with SAP S/4HANA |
| Domain | Typical attributes | Main consumers |
|---|---|---|
| Customer / party | Name, identifiers, addresses, contacts, consent, segment, household | CRM, marketing, service, risk, analytics |
| Product | SKU, description, category, attributes, pricing, packaging | E-commerce, ERP, supply chain (often with a PIM tool) |
| Supplier / vendor | Legal name, tax ID, bank details, certifications, risk rating | Procurement, payables, compliance |
| Employee | Person, role, org unit, manager, location | HR, IT access, payroll |
| Location | Sites, stores, warehouses, geocodes | Logistics, sales territories |
| Asset | Equipment, serial numbers, maintenance hierarchy | Operations, maintenance |
| Financial | Chart of accounts, legal entities, cost centres | Finance, consolidation, regulatory reporting |
Interview-ready summary
"MDM, master data management, creates one trusted golden record for core shared entities such as customer, product and supplier, which otherwise exist as different, conflicting copies in many systems."
"It works in steps: standardise, match with exact and fuzzy rules, merge under a master ID, apply survivorship rules to pick the best value of each attribute, send uncertain matches to data stewards, then publish the golden record and a cross-reference of source IDs to master IDs."
"It differs from an operational database, which stores data for one application and does not know about duplicates elsewhere, and from a data warehouse, which integrates high-volume transactions for analysis but does not decide which record is the real customer. MDM handles a small volume of master data, is governed by stewards, and often flows two ways, sending corrections back to source systems."
"The three work together: MDM decides who the customer is, the master ID becomes the business key of the conformed customer dimension, and the warehouse joins every fact to it, so reports count each customer once."
Follow-up interview questions
Interviewers rarely stop at "what is MDM?". These are the follow-ups that usually come next, with answers at the depth a senior or architect role expects.
Q1. What is the difference between MDM and data governance?
Data governance is the framework for all data: policies, standards, roles (owners, stewards), decision rights and quality rules. MDM is one programme that applies that framework to master data, with its own processes and tools.
| Data governance | MDM | |
|---|---|---|
| Scope | All data: master, transactional, analytical, AI | Master and reference data only |
| Output | Policies, glossary, ownership, quality standards, access rules | Golden records, cross-reference, hierarchies |
| Nature | Organisational: people and decisions | Operational: processes and a platform |
| Tools | Collibra, Purview, Unity Catalog, Alation | Informatica MDM, Reltio, SAP MDG, Profisee |
They depend on each other: governance decides who owns customer data, which attributes are critical and which source is trusted; MDM enforces those decisions in its match and survivorship rules. MDM without governance becomes an expensive duplicate-matching tool that nobody trusts.
Q2. What is a golden record?
The golden record (also "best version of truth") is the single, most accurate version of an entity, built by matching records from several sources and choosing the best value for each attribute. It usually does not equal any one source record: in the example above, Asha's golden record takes the name from the CRM, the email from the web shop and the latest city.
- A stable master ID that never changes, even when sources do
- Best attribute values, each with lineage: which source supplied it and when
- Links to every contributing source record (the cross-reference)
- Status and history: active, merged, retired; versions over time
- Relationships and hierarchies: household, parent company, product family
Q3. What is the difference between MDM and a Customer 360 platform?
MDM answers "who is this customer?": a governed, high-precision identity with core attributes. A Customer 360 answers "everything about this customer": the identity plus orders, payments, service cases, marketing interactions, web behaviour and scores, usually for front-office users.
| MDM | Customer 360 / CDP | |
|---|---|---|
| Focus | Identity and core attributes | Full profile: identity + transactions + interactions + behaviour |
| Matching | High precision, steward-reviewed; false merges are costly | Often probabilistic identity stitching of cookies and devices, tolerant of some error |
| Users | Operational systems, data teams, compliance | Sales, service, marketing |
| Governance | Strong: owners, stewards, audit | Lighter, driven by activation needs |
| Examples | Informatica MDM, Reltio, SAP MDG | Salesforce Data Cloud, Segment, Adobe Real-Time CDP |
Best practice: build the Customer 360 on top of MDM. The master ID is the spine that every transaction and interaction is attached to. A 360 without MDM shows three half-profiles of the same person.
Q4. What are matching and survivorship rules?
Matching rules decide whether two records are the same entity. Survivorship rules decide which value wins for each attribute once they are matched.
| Matching approach | How it works | Example |
|---|---|---|
| Deterministic | Exact match on standardised strong identifiers | Same tax ID, LEI, passport number or verified email |
| Probabilistic | Weighted score across many attributes (Fellegi-Sunter model); fuzzy measures such as Jaro-Winkler, edit distance, phonetic codes | Name 0.92 similar + same date of birth + same postcode → score 0.95 |
| ML-based | A model trained on steward decisions predicts match probability | Zingg, Reltio and Informatica use ML matching |
Scores fall into three bands: auto-merge above an upper threshold, steward review in the grey zone, no match below. Blocking compares only records sharing a cheap key, such as postcode or the first letters of a surname, to avoid comparing every record with every other.
| Survivorship rule | Picks | Typical use |
|---|---|---|
| Source trust | Value from the most trusted source for that attribute | Legal name from the KYC system |
| Most recent | Latest updated value | Email, phone, address |
| Most complete | Longest or most populated value | Full name over an initial |
| Most frequent | Value most sources agree on | Date of birth |
| Validated | Value that passed external verification | Postal-verified address |
| Steward override | A human decision, which always wins | Resolved conflicts |
Two golden rules: never let a NULL overwrite a real value, and define survivorship per attribute, not per record.
Q5. What is the difference between registry and consolidation MDM?
| Registry | Consolidation | |
|---|---|---|
| Stores | Only IDs, match links and the cross-reference | A physical copy of the cleansed, merged golden records |
| Golden view | Assembled at read time from the sources (virtual) | Pre-built and stored in the hub |
| Source systems | Untouched | Untouched: corrections do not flow back |
| Data quality | Sources stay as they are; view is only as good as runtime rules | Cleansed and enriched in the hub |
| Performance | Depends on calls to every source | Fast reads from the hub |
| Effort and risk | Lowest; quickest to start | Moderate |
| Best for | Quick single view, many systems you cannot change, data that must stay in place | Analytics, reporting and a trusted dim_customer in the warehouse |
Both are read-only towards the sources. If sources must also be corrected, move to coexistence; if all creation must be controlled, move to centralised. Many companies start with registry or consolidation and evolve.
Q6. How do you handle conflicting customer records from different systems?
- Standardise first: many "conflicts" are formatting, such as "+91 98450 12345" vs "98450-12345"
- Agree source trust per attribute with the data owners: KYC system for legal name and date of birth, billing for address, CRM for contact preferences
- Apply survivorship rules, recording which source won each attribute and why
- Route true conflicts to stewards: two trusted sources with different dates of birth should not be auto-resolved
- Keep everything: losing values stay in history and the cross-reference, so decisions can be audited and reversed
- Support unmerge: a wrong merge must be undoable, with facts re-pointed to the right master IDs
- Fix at source: in coexistence style, send corrections back so the conflict does not return tomorrow
- Measure: duplicate rate, false-merge rate, steward backlog and time to resolve
Q7. How do you maintain historical changes in master data?
Master data needs three kinds of history: attribute history (Asha moved from Pune to Mumbai), identity history (records merged or unmerged) and an audit trail (who changed what, and which steward approved it).
Attribute history is usually kept as SCD Type 2 on the golden record. This Delta Lake MERGE closes the old version and inserts the new one in one atomic step:
MERGE INTO golden_customer_hist t
USING (
-- every incoming record, keyed so it meets its current row
SELECT u.master_id AS merge_key, u.* FROM golden_updates u
UNION ALL
-- changed records again with a NULL key, so they fall through to INSERT
SELECT NULL AS merge_key, u.*
FROM golden_updates u
JOIN golden_customer_hist h
ON h.master_id = u.master_id AND h.is_current
AND h.attr_hash <> sha2(concat_ws('|', u.name, u.email, u.city), 256)
) s
ON t.master_id = s.merge_key AND t.is_current
WHEN MATCHED AND t.attr_hash <> sha2(concat_ws('|', s.name, s.email, s.city), 256) THEN
UPDATE SET valid_to = CAST(s.changed_at AS TIMESTAMP), is_current = false
WHEN NOT MATCHED THEN
INSERT (master_id, name, email, city, attr_hash, valid_from, valid_to, is_current)
VALUES (s.master_id, s.name, s.email, s.city,
sha2(concat_ws('|', s.name, s.email, s.city), 256),
CAST(s.changed_at AS TIMESTAMP), NULL, true);
| master_id | city | valid_from | valid_to | is_current |
|---|---|---|---|---|
| CRM-1 | Pune | 2026-01-10 | 2026-09-01 | false |
| CRM-1 | Mumbai | 2026-09-01 | NULL | true |
| CRM-2 | Delhi | 2026-02-01 | NULL | true (unchanged, untouched) |
| CRM-7 | Chennai | 2026-09-01 | NULL | true (new customer) |
Tested on PySpark 3.5 with Delta Lake 3.2: 1 row updated, 2 inserted. Re-running the same batch changed nothing, because the attribute hash detects that nothing changed. Asking for CRM-1's city on 2026-05-01 returned Pune.
- Identity history: keep the cross-reference with valid_from and valid_to too, so a merge or unmerge on a date can be replayed and old reports reproduced
- Audit trail: an append-only change log of every edit, merge and steward decision with user and reason
- Bitemporal history for regulated industries: valid time (when it was true in the real world) and system time (when we recorded it), so you can answer "what did we know on 31 March?"
- Do not rely on Delta time travel for business history: VACUUM and retention settings remove old versions
Q8. What is the difference between MDM and a data warehouse?
A data warehouse integrates large volumes of transactional history from many systems for analysis. It is one-way and read-only towards the sources, and it inherits whatever duplicates arrive. MDM manages a small volume of core entities to resolve and govern identity: it matches, merges and applies survivorship, involves human stewards, and can push corrections back to operational systems in real time.
They are complementary, not alternatives: MDM produces the master ID and golden attributes; the warehouse uses them as the business key and attributes of conformed dimensions such as dim_customer, then adds the facts. The full comparison is in the table at the top of this section.
Q9. How do you integrate MDM with Kafka and a lakehouse?
SOURCES KAFKA MDM HUB
CRM, core banking, CDC crm.customer.cdc ---> match, merge,
web, loyalty ----------> web.customer.cdc survivorship,
(Debezium, GoldenGate) loyalty.customer.cdc stewardship
|
mdm.customer.golden <-----------+ compacted topic,
mdm.customer.xref <-----------+ key = master_id
| |
+--------------+ +--------------------+
v v
operational apps update LAKEHOUSE (Structured Streaming)
their local copies bronze: raw golden and xref events
silver: golden_customer_hist (SCD2 MERGE)
xref_hist
gold: dim_customer, Customer 360
- Inbound: change data capture (Debezium, Oracle GoldenGate, Qlik) streams every source change into one topic per source, so MDM sees changes in seconds instead of nightly files
- Outbound: the hub publishes golden-record events to a log-compacted topic keyed by master_id, so the topic always holds the latest state of every customer and new consumers can rebuild from it. Merges and unmerges go to a separate xref topic
- Ordering: keying by master_id puts all events for one customer in one partition, which keeps them in order
- Contracts: Avro or Protobuf schemas in a schema registry, with compatibility rules, so producers cannot break consumers
- Lakehouse: Spark Structured Streaming reads the topics into bronze, then each micro-batch runs the SCD2 MERGE above in
foreachBatch; the checkpoint plus an idempotent MERGE gives exactly-once results in Delta - Merges in facts: store source IDs in facts and resolve master IDs through the cross-reference when building gold, so a later merge or unmerge re-points history without rewriting fact tables
def upsert_golden(batch_df, batch_id):
batch_df.createOrReplaceTempView("golden_updates")
batch_df.sparkSession.sql(HIST_MERGE_SQL) # the SCD2 MERGE shown above
(spark.readStream.format("kafka")
.option("kafka.bootstrap.servers", "broker:9092")
.option("subscribe", "mdm.customer.golden")
.load()
.select(F.from_json(F.col("value").cast("string"), golden_schema).alias("e"))
.select("e.*")
.writeStream
.foreachBatch(upsert_golden)
.option("checkpointLocation", "/chk/golden_customer_hist")
.start())
The MERGE inside foreachBatch is the one tested above. The Kafka reader follows the standard Spark Structured Streaming API but was not run here, as it needs a broker. If one batch can hold several events for the same master_id, keep only the latest per key before the MERGE.
Q10. How would you implement MDM for customer and counterparty data in a global bank?
Start with why the bank needs it: regulation and risk. Credit exposure must be aggregated per counterparty and per group (BCBS 239, large-exposure rules), KYC and anti-money-laundering rules require knowing the customer and its beneficial owners, and sanctions screening needs one reliable identity. A false merge in a bank is not a marketing nuisance: it can misstate risk or leak one client's data to another.
| Design area | What I would do |
|---|---|
| Data model | A party model: one party (person or legal entity) with many roles (customer, counterparty, guarantor, issuer, beneficial owner) and relationships, rather than separate customer and counterparty silos |
| Identifiers | Deterministic matching on strong IDs: LEI (ISO 17442, 20 characters), SWIFT BIC, company registration and tax numbers; national IDs for individuals, tokenised |
| Hierarchies | Legal-entity hierarchy with direct and ultimate parent, enriched from GLEIF Level 2 "who owns whom" data and vendors such as Dun & Bradstreet or Moody's Orbis, so exposure rolls up to the group |
| Matching | Deterministic first, then probabilistic on name and address with legal-suffix normalisation (Ltd, Limited, GmbH) and transliteration of non-Latin scripts; conservative auto-merge thresholds and four-eyes steward approval |
| Survivorship | KYC system authoritative for legal identity and date of birth; GLEIF for legal name and parent; CRM for contact preferences; steward overrides audited |
| Style | Centralised or coexistence for counterparty onboarding, run as a collaborative KYC workflow; consolidation first for retail customers across many legacy core systems |
| Data residency | Regional hubs where law requires local storage (for example EU GDPR transfer rules, India's RBI payment-data localisation, China's PIPL), with a global index holding only pseudonymised keys and links |
| History | Bitemporal history and a full audit trail, kept for the regulatory retention period |
| Integration | CDC from core banking, cards, lending and trading into Kafka; golden and xref events back to them; a lakehouse for risk aggregation, regulatory reporting and Customer 360 |
| Screening | Sanctions and PEP screening (OFAC, UN, EU lists) runs on the golden record and its owners, re-run on every change |
| Security | PII encrypted and tokenised, attribute-level access by role and region, consent tracked |
| Governance | A client data office with owners per domain, critical data elements, data quality KPIs (duplicate rate, false-merge rate, LEI coverage, KYC completeness) and BCBS 239 evidence |
Roadmap: (1) legal-entity counterparty master for risk, with the highest regulatory value; (2) hierarchies and beneficial ownership; (3) retail customer master region by region; (4) real-time operational sync and Customer 360. Each phase ships golden records that a regulator-facing report actually uses.
25. Observability and Operations
A platform in production needs the same discipline as any software system. Data observability watches the data itself, not just whether jobs succeeded, because a job can succeed while writing wrong data.
| Pillar | Question | Signal |
|---|---|---|
| Freshness | Is the data up to date? | Time since last update |
| Volume | Did we get the expected amount? | Row counts against history |
| Schema | Did the structure change? | Added, removed or retyped columns |
| Distribution | Do the values look normal? | Null rates, ranges, category mix |
| Lineage | What is affected? | Upstream causes, downstream impact |
SLIs, SLOs and SLAs
| Term | Meaning | Example |
|---|---|---|
| SLI (indicator) | What you measure | Minutes after 06:00 that gold sales tables were ready |
| SLO (objective) | Internal target | Ready by 07:00 on 99% of days |
| SLA (agreement) | Promise to consumers, often with consequences | Ready by 08:00 or the business is notified |
Tools
| Need | Options |
|---|---|
| Data observability | Monte Carlo, Bigeye, Soda, Elementary (dbt), Lakehouse Monitoring |
| Pipeline and infrastructure monitoring | Datadog, Grafana, cloud monitoring services |
| Platform usage and cost | System tables and account usage views on each platform |
| Incidents | PagerDuty, Opsgenie, ServiceNow |
Operating model
- Everything as code: infrastructure with Terraform, Bicep or CloudFormation; jobs with Asset Bundles, dbt or similar; permissions as code where possible
- Environments: dev, test and prod separated by account, workspace or catalog, with production data access restricted
- CI/CD: every change reviewed and tested before deployment, including data tests on representative data
- Runbooks and on-call: a documented response for each critical pipeline, and blameless reviews after incidents
- Change management: data contracts and lineage-based impact checks before breaking changes
26. Resilience, Lifecycle and Cost
Disaster recovery
Recovery is designed from the two numbers agreed earlier. RPO, the recovery point objective, is how much data you can afford to lose. RTO, the recovery time objective, is how long you can be down.
| Approach | RPO and RTO | Cost |
|---|---|---|
| Backups restored when needed | Hours to days | Low |
| Copy data to a second region on a schedule | Hours | Medium |
| Warm standby in a second region | Minutes to an hour | High |
| Active in two regions | Near zero | Highest |
-- A full, independent copy of a Delta table, for backup or another region CREATE OR REPLACE TABLE dr_sales.gold.fact_order_line DEEP CLONE prod_sales.gold.fact_order_line;
| Platform | Recovery features |
|---|---|
| Databricks | Deep clone, cloud storage replication, workspace configuration as code |
| Snowflake | Time Travel and Fail-safe, database replication and failover across regions |
| BigQuery | Time travel window, cross-region dataset replication |
| Object storage | S3 cross-region replication, Azure geo-redundant storage, GCS dual and multi-region buckets |
| Databases | Read replicas, point-in-time restore, cross-region replicas |
Delta time travel helps with mistakes such as a bad overwrite, but it is not a backup: the data is in the same storage, and VACUUM removes old versions. Also store infrastructure and jobs as code, so the platform itself can be rebuilt.
Data lifecycle and retention
| Stage | Retailer rule |
|---|---|
| Hot | Last 13 months of orders in gold for fast queries |
| Warm | Older orders in a cheaper storage tier |
| Archive | Up to the legal retention period, rarely read |
| Delete | After retention ends, or when the law requires it |
Cost as an architecture concern
- Tag compute and tables by team and project, so every cost has an owner
- Report spend from billing data, for example the Databricks
system.billing.usagetable - Use serverless or autoscaling for uneven workloads, and job compute rather than all-purpose clusters for scheduled work
- Set budgets and alerts, and remove tables that lineage shows nothing reads
- Review the biggest jobs every month: a few usually cause most of the bill
Sharing data
Instead of copying data to partners, Delta Sharing, an open protocol, gives them read access to live tables, with the same governance and audit. Data mesh takes the idea further inside a company: each domain publishes data products, which are documented, owned and quality-checked datasets that others can use. The Data Architecture Patterns card covers data mesh in more detail.
27. Data Architecture for AI
AI raises the stakes for everything above. A model trained on personal data, or a chatbot that retrieves documents a user should not see, turns a governance gap into a visible incident.
| Component | Purpose | Examples |
|---|---|---|
| Feature store | Reusable, consistent features for training and serving | Databricks Feature Store, SageMaker Feature Store, Vertex AI Feature Store, Feast |
| Vector database | Stores embeddings for semantic search and retrieval | pgvector, OpenSearch, Azure AI Search, Mosaic AI Vector Search, Pinecone |
| Model registry | Versioned models with lineage to data and code | MLflow, SageMaker, Vertex AI, Azure ML |
| Monitoring | Drift, quality and safety of predictions | Platform monitoring tools, Evidently |
Governance rules for AI data
- Permission-aware retrieval: a retrieval-augmented (RAG) assistant must only return documents the asking user may read; filter by the user's permissions at query time, not when building the index
- Redact before embedding: remove or mask personal data before documents become embeddings, since embeddings are hard to delete selectively
- Lineage from data to model: record which data version trained each model, so you can retrain after an erasure request or a quality issue
- Purpose and consent: data collected for orders may not be lawful to use for training without a proper basis
- Regulation: the EU AI Act sets duties by risk level, phased in from 2025, so classify AI use cases as well as data
28. Choosing Technology
How to evaluate a platform
Score options against agreed criteria rather than preferences. Weight the criteria first, with stakeholders, then score each option, and record the result in an ADR.
| Criterion | Questions |
|---|---|
| Fit | Does it meet the non-functional requirements and main use cases? |
| Total cost of ownership | Licences, compute, storage, people, training and migration together |
| Openness | Open formats and APIs? How hard is it to leave? |
| Security and compliance | Certifications, regions, encryption, audit |
| Skills | Can the current team use it, or hire for it? |
| Ecosystem | Connectors, partners, community |
| Vendor | Roadmap, support, financial stability |
Build, buy or assemble
| Option | Good when | Risk |
|---|---|---|
| Buy a managed platform | Speed matters and needs are common | Cost and lock-in |
| Assemble open source | Strong engineering team, unusual needs | Operating effort |
| Build custom | It is a genuine competitive advantage | Maintenance forever |
29. Implementing an Enterprise Data Lake
A data lake is cheap object storage plus the rules that stop it becoming a data swamp: files nobody can find, trust or delete. The storage is the easy part; the conventions are the architecture.
Zones
| Zone | Contents | Who writes | Who reads |
|---|---|---|---|
| Landing | Files exactly as delivered, kept briefly | Ingestion tools | Ingestion jobs only |
| Raw (bronze) | Immutable copy of every source, with load metadata | Ingestion jobs | Engineers |
| Curated (silver) | Cleaned, typed, deduplicated, conformed | Pipelines | Engineers, analysts |
| Consumption (gold) | Business-ready, modelled, documented | Pipelines | Everyone with access |
| Sandbox | Personal and team experiments, auto-expiring | Analysts, scientists | Their owners |
| Archive | Old data kept for legal reasons | Lifecycle rules | Rarely, on request |
Storage layout
Separate by environment and zone at the bucket or container level, because that is where cloud permissions, encryption keys and lifecycle rules apply. Use a fixed path convention below that.
s3://acme-lake-prod-raw/ one bucket per environment and zone crm/customers/ingest_date=2026-11-07/part-0000.parquet orders_db/orders/ingest_date=2026-11-07/... s3://acme-lake-prod-curated/ sales/orders/ table format folder (Delta or Iceberg) s3://acme-lake-prod-gold/ sales/fact_order_line/ pattern: <env>-<zone> / <source or domain> / <dataset> / <partition>
Implementation steps
1. Set up the landing zone. Cloud accounts or subscriptions per environment, network isolation, encryption with managed keys, central logging. Do this before any data arrives.
2. Create zones with their own permissions. Ingestion roles can write raw; only pipelines write curated and gold; analysts read gold. Deny public access everywhere.
3. Register everything in a catalog. Glue Data Catalog, Unity Catalog, Purview or Knowledge Catalog. A file that is not in the catalog does not exist for consumers.
4. Standardise ingestion. One framework, with a metadata contract per source: owner, classification, schema, frequency, retention. Reject sources without an owner.
5. Choose formats. Keep raw in the delivered format or Parquet; use an open table format from curated onwards, so you get updates, deletes and schema evolution.
6. Apply lifecycle rules. Landing files expire in days, sandbox in weeks, raw moves to cheaper storage classes after months, archive follows legal retention.
7. Add quality and lineage. Checks between zones; lineage captured automatically where possible.
8. Publish and measure. Track the share of datasets with an owner, a description and a classification. Falling numbers are the first sign of a swamp.
| Building block | AWS | Azure | Google Cloud |
|---|---|---|---|
| Storage | S3 | ADLS Gen2 | Cloud Storage |
| Catalog and permissions | Glue Data Catalog, Lake Formation | Purview, storage RBAC and ACLs | Knowledge Catalog, IAM |
| Query | Athena, Redshift Spectrum | Synapse serverless, Fabric | BigQuery external and BigLake tables |
| Processing | Glue, EMR | Databricks, Fabric Spark | Dataproc, Dataflow |
30. Implementing an Enterprise Data Warehouse
A warehouse gives the business one trusted, modelled version of its data, optimised for SQL and BI. Three classic design approaches still shape how it is built:
| Approach | Idea | Strengths | Weaknesses |
|---|---|---|---|
| Kimball (bottom-up) | Build star-schema marts per business process, joined by conformed dimensions | Fast value, easy for BI users | Needs discipline to keep dimensions conformed |
| Inmon (top-down) | A normalised enterprise warehouse first, then marts from it | Integrated, consistent core | Slow to deliver |
| Data Vault 2.0 | Hubs (keys), links (relationships) and satellites (history) in the core, marts on top | Auditable, absorbs source changes, parallel loading | Complex; still needs marts for users |
Sources ─► Staging ─► Integration layer ─► Data marts ─► Semantic layer ─► BI
(as is) (3NF or Data Vault) (star schemas) (metrics)
The bus matrix
Kimball's bus matrix is the planning tool: business processes as rows, shared dimensions as columns. Each tick is a fact table that must use that conformed dimension, and the matrix becomes the delivery roadmap.
| Business process | Date | Customer | Product | Store | Supplier |
|---|---|---|---|---|---|
| Orders | ✓ | ✓ | ✓ | ✓ | |
| Returns | ✓ | ✓ | ✓ | ✓ | |
| Inventory | ✓ | ✓ | ✓ | ✓ | |
| Purchasing | ✓ | ✓ | ✓ |
Implementation steps
1. Start from business questions. Interview stakeholders, list the key metrics and decisions, and build the bus matrix.
2. Choose the core approach and platform. Kimball for speed, Data Vault when sources change often and audit matters; then a platform from the evaluation criteria in this card.
3. Build conformed dimensions first. Date, customer and product, with surrogate keys and SCD rules agreed per attribute.
4. Deliver one process at a time. Typically the highest-value row of the bus matrix, end to end, including tests and a dashboard.
5. Load incrementally. CDC or watermarks into staging, MERGE into the core, idempotent at every step.
6. Manage workloads. Separate compute for loading, BI and ad hoc queries; materialised views for hot aggregates.
7. Secure and govern. Role-based access, masking of personal data, documented metrics in the semantic layer.
The Data Warehouse card covers star and snowflake schemas, SCDs and normal forms; the Data Modelling card builds a full mart with DDL and PySpark.
31. Implementing a Lakehouse on Delta Lake
A lakehouse combines the lake's cheap open storage with the warehouse's reliability, using an open table format. Here is an enterprise implementation on Delta Lake with Unity Catalog, followed by the Iceberg equivalent.
Ingestion Storage and catalog Consumption
───────── ─────────────────── ───────────
CDC, Auto Loader ─► prod catalog ─► SQL warehouses, BI
Kafka streams ├─ bronze raw, append-only ML, feature store
Batch connectors ├─ silver cleaned Delta Sharing
└─ gold marts, products Semantic layer, AI
Unity Catalog: access, lineage, audit, tags
Step 1: lay out catalogs and schemas
Decide early how catalogs map to the organisation, because permissions and isolation follow this structure. Two common designs: one catalog per environment with schemas per layer, or one catalog per domain per environment.
CREATE CATALOG IF NOT EXISTS prod_sales MANAGED LOCATION 's3://acme-lakehouse-prod/sales'; CREATE SCHEMA IF NOT EXISTS prod_sales.bronze; CREATE SCHEMA IF NOT EXISTS prod_sales.silver; CREATE SCHEMA IF NOT EXISTS prod_sales.gold; GRANT USE CATALOG ON CATALOG prod_sales TO `sales-analysts`; GRANT USE SCHEMA, SELECT ON SCHEMA prod_sales.gold TO `sales-analysts`;
Step 2: set table standards
Write the defaults once and apply them to every table through templates or code review.
CREATE TABLE prod_sales.silver.orders ( order_id STRING NOT NULL COMMENT 'Business key from the order system', customer_id STRING, order_ts TIMESTAMP COMMENT 'UTC', amount DECIMAL(18,2), _ingested_at TIMESTAMP COMMENT 'Load time, for audit' ) CLUSTER BY (order_ts) COMMENT 'One row per order. Owner: sales-domain' TBLPROPERTIES ( 'delta.enableChangeDataFeed' = 'true', 'delta.deletedFileRetentionDuration' = 'interval 7 days', 'delta.logRetentionDuration' = 'interval 30 days' );
| Standard | Why |
|---|---|
| Managed tables in Unity Catalog | Governance, lineage and automatic maintenance |
| Liquid clustering, not partitioning, for most tables | Databricks guidance for tables under 1 TB, and flexible later |
| Comments and owners on every table | Discoverability and accountability |
| Retention properties set deliberately | They decide time travel and how long deleted personal data survives |
| UTC timestamps, DECIMAL money, audit columns | Consistency across domains |
Step 3: ingestion and processing
- Bronze: Auto Loader for files, Lakeflow Connect or CDC tools for databases, Structured Streaming for Kafka; append-only
- Silver: deduplicate, type and conform; MERGE for idempotency
- Gold: dimensional models or data products, built with SQL, dbt or Lakeflow Declarative Pipelines
- Orchestrate with Lakeflow Jobs or Airflow; deploy with Asset Bundles through CI/CD
Step 4: operate it
- Maintenance: enable predictive optimization, or schedule OPTIMIZE, VACUUM and ANALYZE
- Quality: expectations, dbt tests or monitoring on gold tables
- Cost: tagged compute per team, serverless where usage is uneven
- Security: groups, tag-based masking, audit reviews
The same lakehouse on Iceberg
| Concern | Delta Lake on Databricks | Iceberg (open or multi-engine) |
|---|---|---|
| Catalog | Unity Catalog | Glue Data Catalog, Apache Polaris, Snowflake Horizon, Unity Catalog |
| Engines | Databricks, Spark | Spark, Trino, Flink, Athena, Snowflake, BigQuery |
| Layout | Liquid clustering | Hidden partitioning, sort orders, partition evolution |
| Compaction | OPTIMIZE | rewrite_data_files procedure |
| Removing old data | VACUUM | expire_snapshots, then remove_orphan_files |
| Change feed | Change data feed | Incremental reads between snapshots; row lineage in v3 |
32. Implementing a Data Mesh
Data mesh is mostly an organisational change, not a product you install. It answers a scaling problem: one central data team becomes a bottleneck when hundreds of sources and consumers depend on it.
| Principle | Meaning | In practice |
|---|---|---|
| Domain ownership | The teams who know the data own it end to end | Sales owns sales data products, including quality and support |
| Data as a product | Data is published with the care of a product | Owner, documentation, SLOs, contract, version |
| Self-serve platform | A platform team makes publishing easy | Templates for pipelines, catalogs, CI/CD, access |
| Federated computational governance | Shared rules, enforced automatically | Policies as code, tag-based security, standard checks |
Is it right for you?
| Signal | Mesh helps | Mesh is premature |
|---|---|---|
| Organisation | Many domains, many data teams | One small data team |
| Bottleneck | Central team backlog of months | Delivery is fast enough |
| Domain skills | Domains have or can hire engineers | Domains have no technical staff |
| Platform maturity | Strong platform and governance automation | Basics such as a catalog are missing |
A data product specification
Every product is described in a standard, machine-readable file, so the platform can validate it, publish it to the catalog and enforce its policies.
name: sales.orders_daily
domain: sales
owner: sales-data-team@acme.example
description: Daily orders by store and product, net of returns
output_ports:
- type: table
location: prod_sales.gold.orders_daily
- type: share
name: orders_daily_partners
schema_contract: contracts/orders_daily.yaml # columns, types, compatibility
classification: internal
slo:
freshness: "07:00 daily"
completeness: ">= 99.9% of stores"
version: 2.1.0
Implementation steps
1. Pick two or three domains. Choose domains with strong teams and real consumers, not the hardest problem first.
2. Build the self-serve platform. A product template that creates storage, a catalog or schema, CI/CD, quality checks and access groups from the specification above.
3. Define global standards. Naming, classification tags, security policies, interoperability formats and SLO reporting, enforced by the platform rather than by review meetings.
4. Publish products to a marketplace. A catalog where consumers find products, read contracts and request access.
5. Share between domains without copying. Use the platform's sharing features, so domains consume each other's products in place.
6. Measure and expand. Track product usage, SLO compliance and time-to-publish; add domains as the platform matures.
| Platform | Domain boundary | Sharing between domains |
|---|---|---|
| Databricks | Catalog per domain in Unity Catalog | Grants across catalogs, Delta Sharing |
| Snowflake | Database or account per domain | Secure data sharing, listings |
| AWS | Account per domain | Lake Formation cross-account grants, SageMaker Catalog |
| Azure | Fabric domains and workspaces | OneLake shortcuts, Purview |
| Google Cloud | Project per domain | BigQuery sharing (Analytics Hub), Knowledge Catalog |
Common failures: renaming central teams as "domains" without giving them ownership, building no platform so each domain reinvents pipelines, and skipping governance so products are incompatible. Many companies end up with a hybrid: a central platform and core data, with domains owning their products.
33. Modernising Legacy Systems
Most enterprises do not start from scratch. They have a warehouse appliance, a Hadoop cluster, decades of ETL jobs and perhaps a mainframe. Modernisation moves these to a cloud platform without breaking the business while it happens, which is why it is a programme of waves, not a single project.
Why modernise
- Hardware or licence end of life, and rising support costs
- Fixed capacity that cannot scale for month-end or AI workloads
- Skills scarcity for old technologies
- No support for streaming, unstructured data or machine learning
- Governance gaps: no lineage, coarse access control
Typical legacy estates and where they go
| Legacy | Common target | Approach and tools |
|---|---|---|
| Teradata, Netezza, Exadata, Oracle or SQL Server warehouses | Snowflake, BigQuery, Databricks, Redshift or Fabric | Automated SQL conversion (Databricks Lakebridge, Snowflake SnowConvert, BigQuery SQL translation, AWS Schema Conversion Tool), then manual fixes |
| Hadoop: Hive, Impala, HDFS | Lakehouse on Delta or Iceberg | Copy data to object storage, convert Parquet tables in place, port HiveQL, replace Oozie with Airflow or jobs |
| ETL tools: Informatica PowerCenter, DataStage, SSIS, Ab Initio | dbt, Spark, Data Factory, Glue | Converters where available, or rebuild using lineage to prioritise |
| Mainframe: DB2, IMS, VSAM, COBOL batch | Cloud data platform, mainframe kept as system of record at first | CDC replication of DB2; parse EBCDIC files with COBOL copybooks, for example with the Cobrix library for Spark |
| On-premise OLTP databases | Managed cloud databases | Database migration services plus CDC into analytics |
| Reporting: Business Objects, Cognos, MicroStrategy, SSRS | Power BI, Tableau, Looker | Rebuild on a semantic layer; retire unused reports |
Migration tools change quickly: Databricks made the BladeBridge conversion technology free as Lakebridge in 2025, and Snowflake now centres on SnowConvert AI. Always run a proof of concept on your own code, because conversion rates vary widely by source.
Modernisation patterns
| Pattern | How it works | When |
|---|---|---|
| Strangler fig | Replace the legacy system piece by piece, routing one domain or report at a time to the new platform | Default for large estates: low risk, value early |
| Coexistence with CDC | Keep the legacy source running and stream its changes to the new platform | Mainframes and core databases that cannot move yet |
| Big bang | Move everything and switch over at once | Small, simple estates only |
| Lift and shift first | Move as is, modernise later | A hard data-centre exit date |
The six Rs of migration
| Strategy | Meaning | Example |
|---|---|---|
| Rehost (lift and shift) | Move as is to cloud servers | Oracle on a cloud VM |
| Replatform | Small changes to use managed services | Oracle to a managed database service |
| Refactor or re-architect | Redesign for the cloud | Teradata warehouse to a lakehouse |
| Repurchase | Replace with a SaaS product | Custom CRM to Salesforce |
| Retire | Switch off what nobody uses | Old reports found through lineage |
| Retain | Keep on premise for now | Mainframe with strict constraints |
A modernisation programme, phase by phase
1. Discover. Inventory every table, job, report and user. Usage logs and lineage show what is really used; it is common to find a large share of reports and tables nobody reads, which can be retired instead of migrated.
2. Assess and plan. Score each workload by business value and migration complexity, group them into waves by domain, and build the business case on total cost, including running both systems in parallel.
3. Build the foundation. Landing zone, network, identity, catalog, security policies, CI/CD and the ingestion framework, before the first wave.
4. Migrate a pilot wave. One domain end to end: bulk-load history, keep it current with CDC, convert and test the code, and rebuild its reports.
5. Validate by reconciliation. Prove the new platform gives the same answers as the old one, at several levels (below).
6. Run in parallel, then cut over. Both systems run side by side for an agreed period; consumers switch domain by domain once results match.
7. Decommission. Switch off legacy jobs and hardware wave by wave. Savings only appear when the old system is actually turned off.
Tested: converting legacy Parquet tables in place
Hadoop estates often hold Hive tables already stored as Parquet. Delta can adopt them without rewriting any data, which makes migrating very large tables fast and cheap:
CONVERT TO DELTA parquet.`s3://legacy/warehouse/sales` PARTITIONED BY (region_id INT);
| Check on 100,000 rows in 3 partitions | Result |
|---|---|
| Converting without PARTITIONED BY | Rejected: partition columns found in folder names |
| Converting with PARTITIONED BY | Succeeded |
| Data files | All 6 untouched; only a transaction log was added |
| Reading as Delta | All 100,000 rows |
| Table history | One CONVERT operation |
Iceberg offers similar procedures, such as migrate and snapshot, which add Iceberg metadata over existing files. Test them on a copy first: an in-place conversion changes how the folder must be written from then on.
Tested: reconciling source and target
Reconciliation proves the migration is correct. We copied a table, then deliberately deleted one row and changed one amount in the target. A layered check caught both:
from pyspark.sql import functions as F
def profile(df, key, cols):
row_hash = F.xxhash64(*[F.col(c) for c in cols])
return df.agg(F.count("*").alias("rows"),
F.countDistinct(key).alias("keys"),
F.sum("amount").alias("sum_amount"),
F.sum(row_hash).alias("hash_sum")).first()
s = profile(source, "order_id", ["order_id", "region_id", "amount"])
t = profile(target, "order_id", ["order_id", "region_id", "amount"])
for k in ("rows", "keys", "sum_amount", "hash_sum"):
print(k, "match" if s[k] == t[k] else f"DIFF {s[k]} vs {t[k]}")
missing = source.join(target, "order_id", "left_anti")
changed = source.exceptAll(target).join(target.select("order_id"), "order_id")
| Check | Result | What it found |
|---|---|---|
| Row count | 100,000 vs 99,999 | Something is missing |
| Sum of amount | Differs | A value changed or is missing |
| Hash of all columns | Differs | Any change in any column |
| Missing keys | order 7 | The deleted row |
| Changed rows | order 4242 | The edited amount |
Use the cheap aggregate checks on every table and every load; run the row-level comparison when aggregates disagree, or on a sample. Finish with report-level checks: the top business reports must show the same numbers on both platforms.
What usually goes wrong
| Problem | Example | Prevention |
|---|---|---|
| SQL semantics differ | Integer division, NULL ordering, case-insensitive comparison defaults, date functions | Automated conversion plus targeted tests on edge cases |
| Data types differ | Oracle DATE includes a time; legacy NUMBER precision; CHAR padding | Explicit type mapping document, reviewed per table |
| Time zones | Legacy stored local time without a zone | Convert to UTC once, at ingestion |
| Mainframe encodings | EBCDIC text and packed decimals | Copybook-driven parsing, validated against known totals |
| Hidden dependencies | A spreadsheet macro reading a legacy table | Usage logs, lineage, and asking users before switching off |
| Running two systems too long | Cost doubles and attention splits | Fixed parallel-run periods and decommission dates per wave |
| Copying old problems | Migrating unused tables and bad models as is | Retire first; redesign where it matters |
34. Putting It Together: The Retailer's Plan
Back to the auditor's question. Here is how the architect turns this card into a plan, in an order where each phase makes the next one possible.
| Phase | Work | Answers |
|---|---|---|
| 1. Foundation | A central catalog per platform (Unity Catalog, Lake Formation, Purview, Horizon); identity sync; groups | Where is all our data? |
| 2. Ownership | Owners and stewards per domain; glossary of key terms | Who is responsible? |
| 3. Classification | One tag scheme on every platform; discovery scans; unverified-by-default | What is sensitive? |
| 4. Protection | Least-privilege grants; tag-based masks and row filters; secrets | Who can see emails? |
| 5. Visibility | Native lineage per platform, OpenLineage for custom jobs, one enterprise catalog | Where does email data go? |
| 6. Trust | Quality checks, contracts, monitoring | Is the data right? |
| 7. Compliance | Retention, erasure process, audit reports, access reviews | Can we prove it? |
After phase 5, the auditor's question has a one-query answer: the grants and policies show who can see the email column unmasked, and lineage shows every table it reaches, on every platform. That is the measure of a good data architecture: hard questions get quick, provable answers.
35. Common Mistakes
| Mistake | What goes wrong | Instead |
|---|---|---|
| Buying a tool before defining owners | An empty catalog nobody maintains | Roles and processes first |
| Granting access to individuals | Access piles up as people move | Grant to groups |
| Treating hashing as anonymisation | Emails can be re-identified | Keyed hashing, and treat it as personal data |
| Deleting rows and calling it erasure | Old versions and copies keep the data | DELETE everywhere, then VACUUM |
| Masks written table by table | New tables are missed | Tags with ABAC policies |
| Path-based tables | Weak lineage and governance | Register tables, use names |
| Secrets in notebooks | Leaks through Git and logs | Secret scopes or vaults |
| Assuming lineage is complete | Gaps appear in an audit | Test capture and measure coverage |
| Time travel as a backup | Lost with the storage, or after VACUUM | Deep clones or replication |
| No written decisions | The same debates repeat | Architecture decision records |
36. Common Interview Questions
| Question | Short answer |
|---|---|
| What does a data architect do? | Designs how data is collected, stored, connected, secured and used across the company, and sets the standards teams build on |
| What is data governance? | The roles, rules and processes for owning, defining, protecting and using data |
| Data owner vs data steward? | The owner is accountable and approves access; the steward maintains definitions and quality day to day |
| Privacy vs security? | Security protects data from unauthorised access; privacy makes sure personal data is used lawfully and fairly |
| Masking vs tokenisation vs encryption? | Masking hides values when displayed; tokenisation swaps values for tokens kept in a vault; encryption scrambles data with a key |
| Is a hashed email anonymous? | No. It can be guessed by hashing likely emails, and even a keyed hash is still pseudonymised personal data |
| How do you handle a right-to-erasure request on Delta? | Find every copy through lineage, DELETE from each, then VACUUM after retention so old versions are physically removed |
| RBAC vs ABAC? | RBAC grants by role or group; ABAC applies policies based on attributes such as tags, so they scale to new tables |
| How do row filters and column masks work in Unity Catalog? | A SQL function attached to a table filters rows or changes a column's value per user at query time |
| What is defence in depth? | Several security layers, identity, access, encryption, network and monitoring, so one failure does not expose everything |
| What is data lineage used for? | Impact analysis, root cause analysis, compliance, trust and finding unused tables |
| What are RPO and RTO? | RPO is how much data you can lose; RTO is how long recovery may take |
| Is Delta time travel a backup? | No. It lives in the same storage and VACUUM removes old versions; use deep clones or replication |
| What is a data contract? | An agreement on a dataset's schema, meaning, quality and freshness between producer and consumers |
| What is MDM? | Matching records about the same entity across systems into one golden record |
| What is an ADR? | An architecture decision record: the context, decision, options and impact, written down |
| How do you make governance scale? | Federated ownership, governed tags, ABAC policies, automated lineage and access by group |
| Warehouse vs lake vs lakehouse? | A warehouse holds modelled data in one SQL engine; a lake stores any files cheaply; a lakehouse adds transactions and governance to lake storage with open table formats |
| Data mesh vs data fabric? | Mesh is an organisational model, with domains owning data products; fabric is a technology layer that uses metadata to connect and govern data across systems |
| How do you avoid vendor lock-in? | Open table formats, open catalogs, standard SQL, infrastructure as code, and keeping data in storage you control |
| ETL vs ELT? | ETL transforms before loading; ELT loads raw data and transforms inside the target platform |
| Why use CDC? | It streams inserts, updates and deletes from the transaction log with little load on the source, and keeps copies current |
| How do you keep security consistent across Snowflake, Databricks and S3? | One classification scheme, the same tags everywhere, policies generated from one definition, central audit, and lineage to find unprotected copies |
| How do you handle data residency? | Choose regions per law, keep personal data in-region including backups and logs, and move only aggregated or anonymised data across borders |
| How would you migrate an on-premise warehouse to the cloud? | Inventory with usage data, design the target, move data then sync with CDC, convert logic, run in parallel and reconcile, then cut over by domain and decommission |
| How do you choose between cloud platforms? | Weighted criteria agreed with stakeholders: fit, total cost, openness, security, skills, ecosystem and vendor, recorded in an ADR |
| What changes for AI workloads? | Permission-aware retrieval, redaction before embedding, lineage from data to model, and checking the purpose and consent of training data |
| What is zero trust? | Never trusting a request because of its network location: verify every identity and grant least privilege |
| Delta, Iceberg or Hudi? | Delta when Databricks or Fabric is the main engine; Iceberg when many engines share data or on AWS, Snowflake and BigQuery; Hudi or Paimon for heavy upserts and Flink streaming |
| Backward vs forward compatibility? | Backward: new readers read old data, so upgrade consumers first. Forward: old readers read new data, so upgrade producers first |
| Which schema changes are safe? | Adding optional fields with defaults. Renames, removing required fields and narrowing types break someone |
| How do you make a breaking change safely? | Expand and contract: add the new field, dual-write, migrate consumers using lineage, deprecate, then remove |
| How do you make pipelines idempotent? | MERGE on a business key or overwrite whole partitions, so a rerun produces the same result |
| What is write-audit-publish? | Write to a hidden branch, clone or staging table, validate, then publish atomically |
| How do you isolate workloads? | Separate compute per workload over shared storage, with each tagged and budgeted |
| What is a semantic layer? | Business metrics defined once and reused by every BI and AI tool, so numbers agree |
| What are the pillars of data observability? | Freshness, volume, schema, distribution and lineage |
| SLI vs SLO vs SLA? | The measurement, the internal target, and the external promise |
| Why keep personal data out of Kafka topics? | Retained logs are hard to erase selectively; send IDs and keep details in governed tables |
| How do you stop a data lake becoming a swamp? | Zones with permissions, a mandatory catalog entry with owner and classification at ingestion, lifecycle rules, and quality checks between zones |
| Kimball, Inmon or Data Vault? | Kimball for fast value with conformed star schemas; Inmon for an integrated normalised core; Data Vault for auditable history when sources change often, always with marts on top |
| What is a bus matrix? | Business processes against conformed dimensions; it plans the warehouse and keeps dimensions shared |
| How do you structure catalogs in a lakehouse? | By environment and layer, or by domain and environment; permissions and isolation follow that choice |
| What are the four principles of data mesh? | Domain ownership, data as a product, a self-serve platform, and federated computational governance |
| When is data mesh a bad idea? | Small organisations, domains without technical staff, or a platform that lacks basic governance |
| What is the strangler fig pattern? | Replacing a legacy system piece by piece, routing one part at a time to the new platform |
| How do you validate a migration? | Layered reconciliation: counts, aggregates, column hashes, row-level differences, then the top business reports |
| How do you migrate Hive tables to a lakehouse? | Copy to object storage and convert Parquet tables in place to Delta or Iceberg, adding metadata without rewriting files |
| How do you modernise a mainframe data estate? | Keep it as the system of record at first, replicate with CDC, parse files with copybooks, and move consumers gradually |
| What is the medallion architecture? | Layers of increasing quality in a lakehouse: bronze keeps raw data, silver holds cleaned and conformed entities, gold holds business-ready models |
| Why keep a bronze layer? | It is the replay point and audit record: fix silver logic and rebuild from bronze without asking the source to resend |
| What is the difference between landing and bronze? | Landing holds files exactly as delivered and is short-lived; bronze is a queryable, append-only table of the same data with load metadata, kept long term |
| What is a landing zone? | Either the first drop area for incoming data, or the secure cloud foundation of accounts, networks, identity and policies; know both meanings |
| What is the edge layer? | Devices, sites and systems outside the platform where data is produced; edge gateways buffer, filter, translate and encrypt before sending |
| Where do you apply data quality in medallion? | Silver: expectations validate rows and quarantine failures; bronze keeps everything; gold gets reconciliation and certification |
| Is medallion the same as Kimball? | No. Medallion is about layers of refinement; Kimball is a modelling technique, usually used for gold |
| How do you avoid loading a file twice? | Use tools that track loaded files, such as Auto Loader, COPY INTO or Snowpipe, or a streaming checkpoint |
| What are the biggest challenges in data modelling? | Grain, keys, history and timing: mixed grain, inconsistent IDs across sources, changing attributes, late-arriving data, many-to-many relationships and inconsistent metric definitions |
| How do you handle a late-arriving dimension? | Insert an inferred placeholder member, key the fact to it, and update the placeholder when the real dimension row arrives |
| What happens if you join two fact tables directly? | They usually share only dimensions, so the join is many-to-many: rows multiply, measures are double counted, and an inner join drops members missing from one side |
| Why is a fact-to-fact join slow at scale? | Both sides are shuffled on the join key, the output is the sum of left × right rows per key, hot keys cause skew, and partitions spill or fail |
| How do you combine two facts correctly? | Drill-across: aggregate each fact to the same grain on conformed dimensions, then full outer join; or union the facts and aggregate once |
| What KPIs measure enterprise data quality? | Data KPIs (completeness, validity, uniqueness, consistency, accuracy, reconciliation), delivery KPIs (freshness SLA, MTTD, MTTR, data downtime) and programme KPIs (CDE coverage, ownership, issues found by consumers) |
| What is data downtime? | The total time data was wrong, missing or late: the sum of time to detect plus time to resolve across incidents |
| What is a golden record? | The single best version of an entity, built by matching source records and choosing the best value per attribute, with a stable master ID and lineage |
| What is the difference between MDM and data governance? | Governance sets policies, ownership and standards for all data; MDM applies them to master data to produce governed golden records |
| What are the types of MDM? | By domain (customer, product, supplier, employee, location, asset, financial), by usage (operational, analytical, collaborative) and by style (registry, consolidation, coexistence, centralised) |
| What is MDM? | Master data management: the process and tools that create one trusted golden record for core entities such as customer and product across all systems |
| How is MDM different from a data warehouse? | A warehouse integrates high-volume transactional history for analysis; MDM resolves and governs a small set of core entities and can sync corrections back to source systems. The warehouse uses MDM master IDs as dimension keys |
| What is survivorship in MDM? | Rules that choose the best value of each attribute from matched records, such as the most trusted source, the most recent update or the most complete value |
| What are the MDM implementation styles? | Registry, consolidation, coexistence and centralised (transaction hub), from least to most intrusive |