A hands-on data engineering project built on Databricks and PySpark/Spark SQL, demonstrating the Medallion Architecture (Bronze → Silver → Gold), incremental (delta) loading, a Kimball-style star schema (Dimension & Fact tables), and standalone Slowly Changing Dimension (SCD) Type 1 & Type 2 implementations using MERGE INTO.
This is a learning/portfolio project simulating an e-commerce sales dataset flowing through a lakehouse.
flowchart LR
A[(Source Table\ndatamodeling.default.source_data)] -->|Incremental Extract\nWatermark: order_date| B[Bronze Layer\nbronze.bronze_table]
B -->|Clean & Transform| C[Silver Layer\nsilver.silver_table]
C -->|Dimensional Modeling| D[Gold Layer\nStar Schema]
D --> E[Dim Tables]
D --> F[Fact Table]
| Layer | Purpose | Load Pattern |
|---|---|---|
| Source | Raw transactional table simulating an OLTP system | Manual inserts (initial + incremental) |
| Bronze | Raw ingestion, minimal transformation | Incremental, watermark-based (order_date) |
| Silver | Cleaned, standardized, deduplicated | Merge (upsert) on order_id |
| Gold | Business-ready star schema | Full rebuild of dimensions + fact |
- Platform: Databricks (Notebooks)
- Processing: PySpark, Spark SQL
- Storage: Delta Lake (Unity Catalog:
datamodeling.<layer>.<table>) - Patterns: Incremental loading,
MERGE INTOupserts,ROW_NUMBER()surrogate keys, SCD Type 1 & Type 2
| File | Layer | Description |
|---|---|---|
Source.py |
Source | Creates source_data table and simulates initial + incremental inserts |
Bronze.py |
Bronze | Incrementally pulls new rows from source based on max(order_date) watermark |
Silver.py |
Silver | Cleans/enriches bronze data and merges into the silver table |
Gold.py |
Gold | Builds dimension tables and the fact table (star schema) |
SCDs.py |
Standalone Demo | Implements SCD Type 1 (overwrite) and SCD Type 2 (historical tracking) on a separate product dataset |
Creates datamodeling.default.source_data, a flat table representing raw order transactions (order, customer, product, payment, and region attributes). Data is inserted in two batches — an initial load (3 orders) and an incremental load (2 more orders) — to simulate new data arriving over time.
Implements incremental extraction using a watermark pattern:
- Checks if
bronze.bronze_tablealready exists. - If it does, pulls the
max(order_date)already loaded; otherwise defaults to1000-01-01. - Selects only source rows with
order_dategreater than that watermark. - Writes the result with
CREATE OR REPLACE TABLE, so each run currently replaces the bronze table rather than appending — worth noting if you extend this into a true incremental history.
Transforms bronze data by:
- Adding
customer_name_upper(standardized customer name). - Adding
processDate(load/processing timestamp). - Merging into
silver.silver_tableusingMERGE INTO ... ON order_id, withUPDATE SET *on match andINSERT *on no match — a standard upsert pattern.
Builds a star schema on top of the silver table:
Dimensions
DimCustomers— deduplicated customer attributes, surrogate keyDimCustomerKeyDimProducts— deduplicated product attributes, surrogate keyDimProductKeyDimPayments— deduplicated payment types, surrogate keyDimPaymentKeyDimRegions— deduplicated countries, surrogate keyDimRegionKeyDimSales— order-grain descriptive table, surrogate keyDimSaleKey
Fact
FactSales— grain of one row per order, joining all dimension surrogate keys plus measuresquantityandunit_price.
erDiagram
DimCustomers {
int DimCustomerKey PK
int customer_id
string customer_name
string customer_email
}
DimProducts {
int DimProductKey PK
int product_id
string product_name
string product_category
}
DimPayments {
int DimPaymentKey PK
string payment_type
}
DimRegions {
int DimRegionKey PK
string country
}
DimSales {
int DimSaleKey PK
int order_id
date order_date
}
FactSales {
int DimSaleKey FK
int DimCustomerKey FK
int DimProductKey FK
int DimRegionKey FK
int DimPaymentKey FK
int quantity
decimal unit_price
}
FactSales }o--|| DimSales : references
FactSales }o--|| DimCustomers : references
FactSales }o--|| DimProducts : references
FactSales }o--|| DimRegions : references
FactSales }o--|| DimPayments : references
Note:
DimSalescurrently carries several descriptive columns (customer/product/payment details) that are already normalized into their own dimensions. In a stricter Kimball design you'd trim it down to just the order-level attributes (order_id,order_date,last_updated,processDate) since the rest is redundant with the other dimensions — a good next iteration.
This notebook is a standalone conceptual demo (separate source/target tables from the main pipeline) showing both SCD strategies on a simple product dataset.
- Source:
datamodeling.default.scdtyp1_source - Target:
datamodeling.gold.scdtyp1_table - A single
MERGE INTOupdates matched rows in place (UPDATE SET *) and inserts new ones. Whenprod_catforprod_id = 3is updated, the old value is simply overwritten — no history is kept.
-
Source:
datamodeling.default.scdtyp2_source -
Target:
datamodeling.gold.scdtype2_table(addsstart_date,end_date,is_current) -
Implemented as a two-step merge, which is the standard pattern in Spark SQL since a single
MERGEcan't both expire an old row and insert a new one for the same key:- Merge 1 (Expire): Matches on
prod_id+is_current = 'Y'; if any tracked column changed, setsend_date = current_date()andis_current = 'N'on the old row. - Merge 2 (Insert): Matches on the same key/flag; any row from source that is not matched (i.e. brand-new or just-expired) gets inserted fresh with
is_current = 'Y'andend_date = '3000-01-01'.
This preserves the full change history — each version of a product row is retained with its own validity window.
- Merge 1 (Expire): Matches on
- Import all five
.pyfiles as Databricks notebooks (they use the# Databricks notebook source/# COMMAND ----------cell markers). - Ensure a Unity Catalog catalog/schema structure exists:
datamodeling.default,datamodeling.bronze,datamodeling.silver,datamodeling.gold. - Run in order for the main pipeline:
Source.pyBronze.pySilver.pyGold.py
- Run
SCDs.pyindependently to explore the SCD Type 1 vs Type 2 patterns. - Re-run
Source.py's incremental insert cell, then re-runBronze.py→Silver.py→Gold.pyto see incremental loading and upserts in action.
- ✅ Medallion Architecture (Bronze / Silver / Gold)
- ✅ Incremental/watermark-based extraction
- ✅ Upserts with
MERGE INTO - ✅ Surrogate key generation with
ROW_NUMBER() - ✅ Star schema design (Fact & Dimension tables)
- ✅ SCD Type 1 (overwrite)
- ✅ SCD Type 2 (historical tracking via two-step merge)
- Make Bronze append-only (currently uses
CREATE OR REPLACE) so historical bronze snapshots aren't lost on each run. - Wire the
SCDs.pyType 2 pattern intoDimProductsin the main Gold pipeline, so product history is actually tracked end-to-end. - Parameterize catalog/schema names instead of hardcoding
datamodeling.*. - Add data quality checks (nulls, duplicate PKs) between layers.