Databricks Delta Live Tables: Declarative Pipeline Orchestration Made Simple
If you've ever spent more time debugging broken Spark jobs than actually building pipelines, Databricks Delta Live Tables (DLT) is worth your attention. Instead of writing imperative Spark code that moves data from A to B and hoping nothing breaks in between, DLT lets you declare what your tables should look like and Databricks figures out the rest — dependencies, retries, data quality checks, and all.
In this post I'll walk through building a real-world DLT pipeline from scratch on Databricks, covering everything from raw ingestion to serving-ready Gold tables with data quality expectations baked in.
What is Databricks Delta Live Tables?
DLT is a declarative framework built on top of Delta Lake and Apache Spark. Instead of writing spark.read → transform → spark.write, you define tables using @dlt.table decorators and DLT handles:
- Dependency resolution and execution order
- Incremental vs full refresh modes
- Data quality enforcement via Expectations
- Automatic lineage tracking
- Pipeline monitoring and alerting
The result is pipelines that are easier to read, easier to test, and dramatically easier to recover when something goes wrong.
Architecture Overview
Pipeline Flow
DLT vs Traditional Spark Pipelines
| Feature | Traditional Spark | Databricks Delta Live Tables |
|---|---|---|
| Dependency management | Manual, error prone | Automatic via DAG resolution |
| Data quality | Ad-hoc checks in code | Declarative Expectations |
| Incremental processing | Manual watermark logic | Built-in STREAMING tables |
| Retries and recovery | Custom retry wrappers | Managed by DLT runtime |
| Lineage tracking | None out of the box | Automatic in Unity Catalog |
| Pipeline monitoring | CloudWatch / custom | Built-in DLT event log |
| Schema evolution | Manual mergeSchema | Handled automatically |
Step 1 — Bronze: Raw Ingest with Auto Loader
Auto Loader is DLT's preferred ingest mechanism. It tracks which files have been processed using checkpoints so you never double-ingest.
import dlt
from pyspark.sql.functions import current_timestamp, input_file_name
@dlt.table(
name="bronze_orders",
comment="Raw order events ingested from ADLS Gen2",
table_properties={"quality": "bronze"}
)
def bronze_orders():
return (
spark.readStream
.format("cloudFiles")
.option("cloudFiles.format", "json")
.option("cloudFiles.schemaLocation", "/checkpoints/orders/schema")
.load("abfss://raw@yourstorage.dfs.core.windows.net/orders/")
.withColumn("_ingested_at", current_timestamp())
.withColumn("_source_file", input_file_name())
)Step 2 — Silver: Clean Data with Expectations
Expectations are DLT's data quality layer. You define rules and tell DLT what to do when rows violate them: warn, drop, or fail the pipeline.
import dlt
from pyspark.sql.functions import col, to_timestamp, upper, trim
# Define reusable expectations
ORDER_EXPECTATIONS = {
"valid_order_id": "order_id IS NOT NULL",
"valid_customer_id": "customer_id IS NOT NULL",
"valid_amount": "order_amount > 0",
"valid_status": "status IN ('PENDING', 'CONFIRMED', 'SHIPPED', 'CANCELLED')",
}
@dlt.table(
name="silver_orders",
comment="Cleaned and validated order records",
table_properties={"quality": "silver"}
)
@dlt.expect_all_or_drop(ORDER_EXPECTATIONS) # drop rows that fail any rule
def silver_orders():
return (
dlt.read_stream("bronze_orders")
.withColumn("order_ts", to_timestamp(col("order_timestamp")))
.withColumn("status", upper(trim(col("status"))))
.withColumn("country", upper(trim(col("country_code"))))
.filter(col("order_id").isNotNull())
.select(
"order_id", "customer_id", "order_ts",
"order_amount", "status", "country",
"product_id", "_ingested_at"
)
)
# Quarantine table — keep dropped rows for audit
@dlt.table(
name="silver_orders_quarantine",
comment="Rows that failed data quality expectations"
)
@dlt.expect_all_or_drop({v: f"NOT ({v})" for v in ORDER_EXPECTATIONS.values()})
def silver_orders_quarantine():
return dlt.read_stream("bronze_orders")Step 3 — Gold: Aggregated Business Table
Gold tables are materialized views over Silver. In DLT you define them the same way — just read from Silver instead of Bronze.
import dlt
from pyspark.sql.functions import col, count, sum as _sum, avg, countDistinct, date_trunc
@dlt.table(
name="gold_daily_order_summary",
comment="Daily aggregated order metrics per country and status",
table_properties={"quality": "gold"}
)
def gold_daily_order_summary():
return (
dlt.read("silver_orders")
.withColumn("order_date", date_trunc("day", col("order_ts")))
.groupBy("order_date", "country", "status")
.agg(
count("order_id") .alias("total_orders"),
_sum("order_amount") .alias("total_revenue"),
avg("order_amount") .alias("avg_order_value"),
countDistinct("customer_id").alias("unique_customers"),
)
.orderBy("order_date", "country")
)
@dlt.table(
name="gold_customer_lifetime_value",
comment="Cumulative LTV per customer across all orders"
)
def gold_customer_lifetime_value():
return (
dlt.read("silver_orders")
.filter(col("status") != "CANCELLED")
.groupBy("customer_id")
.agg(
count("order_id") .alias("total_orders"),
_sum("order_amount") .alias("lifetime_value"),
avg("order_amount") .alias("avg_order_value"),
)
)Step 4 — Pipeline Configuration
DLT pipelines are configured via JSON or the Databricks UI. Here's the JSON config you'd use with the CLI or Terraform.
{
"name": "orders-dlt-pipeline",
"target": "orders_catalog.dlt",
"libraries": [
{ "notebook": { "path": "/pipelines/orders_pipeline" } }
],
"clusters": [
{
"label": "default",
"autoscale": {
"min_workers": 2,
"max_workers": 8,
"mode": "ENHANCED"
}
}
],
"continuous": false,
"development": false,
"edition": "ADVANCED",
"photon": true,
"configuration": {
"pipelines.enableTrackHistory": "true"
}
}Things to Watch in Production
Use ADVANCED edition for Expectations. The CORE edition doesn't support data quality Expectations. Make sure your pipeline is set to ADVANCED or you'll get a silent no-op on your quality rules.
Quarantine bad rows, don't just drop them. @dlt.expect_all_or_drop silently removes failing rows. Always pair it with a quarantine table so you have an audit trail of what was dropped and why.
Streaming tables vs materialized views. Use @dlt.table with spark.readStream for Bronze and Silver (incremental). Use @dlt.table with dlt.read (batch) for Gold aggregations. Mixing them incorrectly causes full re-scans on every pipeline run.
Event log is queryable. DLT writes all pipeline events to a Delta table at <storage_location>/system/events. Query it directly to build custom monitoring dashboards in Databricks SQL.
Wrapping Up
Databricks Delta Live Tables removes the scaffolding that makes Spark pipelines painful: manual dependency management, ad-hoc quality checks, and custom retry logic. What you're left with is clean declarative code where every table has a clear definition, a quality contract, and an automatic audit trail.
The Medallion Architecture maps perfectly onto DLT — Bronze for raw ingest, Silver for quality enforcement, Gold for business aggregations — and the combination of Auto Loader and Expectations makes it production-ready with surprisingly little boilerplate.