from pyspark.sql import DataFrame, functions as F
catalog = spark.sql("SELECT current_catalog()").first()[0]
spark.sql(f"USE SCHEMA {catalog}.dbx_course")
Module 22: Industry Standards & Best Practices
Standards and checklists that keep a Databricks codebase maintainable.
Topics in this module
- Databricks Well-Architected Framework
- Clean Code Principles for PySpark
- SOLID Principles in Data Engineering
- Modular Pipeline Design
- Separation of Concerns
- Data Quality Standards
- Naming and Folder Conventions
- Schema and Table Design Guidelines
- Partitioning and Clustering Guidelines
- Observability Standards
- Security and Governance Standards
- CI/CD and Version Control Standards
- Production Readiness Checklist
- Anti-Patterns and Common Mistakes
The standards a team agrees on so that anyone can read, run and trust the code. Most topics are short rules with a good and a bad example. The topics that fit a checklist end with one.
- The Best practices page (in the course map) holds the one-line checklists. This module explains the reasoning and gives examples
- Practice code runs on
orders(Module 4):
Databricks Well-Architected Framework
By the end: use the Databricks well-architected framework as a review lens.
| Pillar | Standard to hold |
|---|---|
| Operational excellence | Code in Git, CI/CD, infrastructure as code, standard compute, monitoring |
| Security, privacy, compliance | Least privilege, secrets in scopes, isolation, auditing |
| Reliability | Delta, idempotent jobs, retries, checkpoints, tested recovery |
| Performance efficiency | Autoscaling or serverless, good table layout, efficient queries |
| Cost optimization | Right compute, tags, budgets, job compute for jobs |
| Data and AI governance | Unity Catalog, lineage, quality rules, owners |
| Interoperability and usability | Open formats, open interfaces, self-service |
- From the docs, operational excellence means: version control, CI/CD, environment isolation by workspace or catalog, managed tables, IaC, monitoring
- Reliability means: Delta for ACID, retry policies and maximum runtimes on jobs, quality checks at ingest, tested recovery (checkpoints, time travel, repair runs)
- Use the pillars as headings in design reviews and post-incident reviews (Module 19.12)
Clean Code Principles for PySpark
By the end: write clean, readable PySpark.
Before:
d = spark.table("orders")
d = d.filter("status = 'completed' or status = 'shipped'")
d = d.withColumn("r", d.quantity * 2)
d = d.groupBy("customer_id").agg(F.sum("r").alias("x"))
After:
REVENUE_STATUSES = ["completed", "shipped"]
def completed_quantity_by_customer(orders: DataFrame) -> DataFrame:
"""Total ordered quantity per customer, counting only revenue-bearing orders."""
return (
orders
.filter(F.col("status").isin(REVENUE_STATUSES))
.groupBy("customer_id")
.agg(F.sum("quantity").alias("total_quantity"))
)
result = completed_quantity_by_customer(spark.table("orders"))
| Rule | Why |
|---|---|
Names say what it is (total_quantity, not x) |
Readers don't decode |
| One chain per step, one transformation per line | Easy to read and diff |
| Functions with a DataFrame in and out | Testable and reusable |
| No magic values: constants with names | One place to change |
| Docstring with grain and meaning | Intent survives |
F.col("...") for expressions, select/withColumns over long withColumn loops |
Clarity and speed |
No collect(), toPandas() on big data |
Memory and speed |
| Comments say why, not what | The code says what |
- Format with a tool (
black/ruff format) and lint (ruff): style is not a debate - Small functions: if you can't name it in a few words, split it
SOLID Principles in Data Engineering
By the end: apply SOLID principles to data code.
| Principle | In a pipeline |
|---|---|
| Single responsibility | A function reads or transforms or writes. Not all three |
| Open/closed | Add behaviour by adding a new step or config entry, not by editing a working function |
| Liskov substitution | Any source (CSV, Delta, API) that implements the reader interface can replace another |
| Interface segregation | Small interfaces: a writer doesn't need to know how to read |
| Dependency inversion | Pass the table names, paths and clients in, don't hard-code them inside |
# dependency inversion: the function gets what it needs
def build_daily(orders: DataFrame, products: DataFrame) -> DataFrame:
return (orders.join(products, "product_id")
.groupBy(F.to_date("order_ts").alias("order_date"))
.agg(F.round(F.sum(F.col("quantity") * F.col("unit_price")), 2).alias("revenue")))
# the caller decides where the data comes from and goes to
daily = build_daily(spark.table("orders"), spark.table("products"))
- The practical test: can I unit-test this function with two tiny DataFrames? If yes, the design is probably sound
- Don't over-engineer: patterns are for repeated problems
Modular Pipeline Design
By the end: design modular pipelines.
def read_orders(spark, table: str) -> DataFrame:
return spark.table(table)
def transform(orders: DataFrame) -> DataFrame:
return completed_quantity_by_customer(orders)
def validate(df: DataFrame) -> DataFrame:
assert df.filter("total_quantity <= 0").count() == 0, "non-positive totals"
return df
def write(df: DataFrame, table: str) -> None:
df.write.mode("overwrite").saveAsTable(table)
def run(spark, source: str, target: str) -> None:
write(validate(transform(read_orders(spark, source))), target)
run(spark, "orders", "std_customer_quantity")
| Layer | Contains | Tested by |
|---|---|---|
| Entry point | Wiring only | Integration test |
| Steps | Read, transform, validate, write | Unit and integration tests |
| Library | Shared pure functions | Unit tests |
- A new source or target changes one function
- Packages, wheels and project layout: Module 26
Separation of Concerns
By the end: separate concerns.
- Business logic doesn't know about tables, paths or schedules
- I/O code doesn't contain business rules
- Environment values (catalog, paths, secrets) are never inside the logic
- Scheduling lives in the job definition, not in
sleeploops in notebooks - Platform setup (compute, grants) is in infrastructure code, not in the pipeline
- Separate concerns = each can change and be tested alone
The 'one big notebook'
Reads, cleans, joins, writes, plots and sends email in 600 lines. Can't test, reuse or review it. Split it.
Data Quality Standards
By the end: set data quality standards.
| Standard | Example |
|---|---|
| Every table has an owner | COMMENT or tag: owner = sales-data |
| Rules per layer | Bronze: shape and arrival. Silver: types, keys, dedupe. Gold: business rules, reconciliation |
| Every rule has an action | Warn, drop (and quarantine), or fail |
| Quarantine, never drop silently | Rejected rows kept with a reason |
| Quality is measured and kept | Counts of failures per run (Module 16.6) |
| SLAs | Freshness and completeness targets, alerted |
| Contracts | Schema, meaning and owner agreed with consumers |
| Constraints where enforceable | NOT NULL, CHECK on Delta tables |
%sql
ALTER TABLE orders_delta SET TBLPROPERTIES ('quality.owner' = 'sales-data', 'quality.sla_hours' = '24')
- A data quality score ("98.7% of rows passed") is a useful trend metric, never the whole story
- Quality rules are code: reviewed, tested, versioned (Module 14)
- Fix problems at the source where you can
Naming and Folder Conventions
By the end: agree on naming and folder conventions.
| Object | Convention | Example |
|---|---|---|
| Catalog | Environment or domain | dev, prod, sales |
| Schema | Domain and layer | sales_bronze, sales_silver, sales_gold |
| Table | Singular or plural, consistent, lowercase snake_case |
orders, dim_customer, fact_orders |
| Column | snake_case, specific names |
order_ts, customer_id, is_valid |
| Keys | <entity>_id, surrogate <entity>_sk |
customer_sk |
| Booleans | is_ / has_ |
is_current |
| Timestamps / dates | _ts, _date suffix, UTC |
created_ts |
| Metadata columns | Underscore prefix | _ingested_at, _source |
| Jobs | <project>-<purpose>-<env> |
orders-daily-prod |
- Lowercase and underscores: avoids quoting and clashes
- Use UTC for timestamps, and say so
- Consistent beats clever. Write the rules in one page and put it in the repo
- Rename with a view or alias and a deprecation period
Schema and Table Design Guidelines
By the end: follow schema and table design guidelines.
| Guideline | Why |
|---|---|
| Delta tables in Unity Catalog, managed by default | Governance, automatic optimization |
| Choose the grain first and write it down | Prevents double counting |
Right types: DATE, TIMESTAMP, DECIMAL(p,s) for money, not strings or floats |
Correct maths, smaller data |
| Nullable only when meaningful | Cleaner logic |
Use NOT NULL and CHECK for rules you can enforce |
Bad writes fail |
| Stable keys, surrogate keys for dimensions | Joins survive source changes |
COMMENTs on tables and columns, tags for sensitivity |
Discovery and governance |
| Avoid wide, deeply nested tables when queries need a few fields | Scan cost |
| Put the columns you filter and join on early (or list them for stats) | Better data skipping |
Store raw JSON in bronze as string or VARIANT, then parse to columns |
Schema changes don't break ingestion |
- Never edit the underlying files of a Delta table
- Use
CREATE OR REPLACE TABLEto rebuild, not delete and recreate - Don't use Spark caching on tables: it loses skipping and can show stale data
Partitioning and Clustering Guidelines
By the end: choose partitioning and clustering by the guideline.
- Databricks recommends liquid clustering for new tables, together with managed tables and predictive optimization
- Clustering keys: the columns you filter on most (up to 4). Not compatible with partitioning or
ZORDER - Partition only very large tables and low-cardinality columns (such as date) where each partition is still large
- Over-partitioning (thousands of tiny partitions) is worse than none
- Run
OPTIMIZEregularly or let predictive optimization do it.OPTIMIZEcompacts files.VACUUMremoves old ones - Don't call
REFRESH TABLEor manage partitions by hand: filter withWHERE
| Table | Use |
|---|---|
| Small (under a few GB) | No partitioning, optional clustering |
| Large, filtered by date and customer | CLUSTER BY (order_date, customer_id) |
| Huge, always filtered by one low-cardinality column | Partition by it, maybe cluster inside |
Observability Standards
By the end: meet observability standards.
| Signal | Standard |
|---|---|
| Logs | Structured, with job, run id, table, date. No secrets or personal data |
| Metrics | Rows in, out, rejected. Duration. Freshness. Per step, stored in a table |
| Traces / lineage | Unity Catalog lineage, event logs, query history |
| Alerts | Failure, slow run, missing run, quality, cost. To a channel, with a link to the run and runbook |
| Dashboards | A pipeline health view per domain |
| Runbooks | One per pipeline, updated after incidents |
| Ownership | Every job and table has an owner and an on-call |
- Alert on symptoms people feel (stale dashboard) and causes you can act on (failed task)
- Every alert has an action. If it doesn't, delete it
- Retention: keep audit and run logs as long as compliance needs
- Detail: Module 16
Security and Governance Standards
By the end: meet security and governance standards.
- Unity Catalog for all data. Three-level names always
- Groups, not individuals, in grants. Least privilege. Owners are groups
- Service principals run production jobs
- Secrets in scopes (or a secret manager). Never in code, notebooks, logs, parameters
- Separate environments by catalog, schema or workspace
- Sensitive data tagged, masked or filtered at the table (row filters, column masks, ABAC)
- Retention and deletion policies written down and implemented (
DELETE,REORG,VACUUM) - Audit access and changes. Review grants regularly
- Network controls where regulation needs them (private connectivity, egress control)
- Governance changes go through code review
%sql
-- a standing review query: who holds powerful privileges on this schema?
SHOW GRANTS ON SCHEMA dbx_course
Default deny
Start with no access and add what each group needs. Removing access later is harder than not giving it.
CI/CD and Version Control Standards
By the end: meet CI/CD and version control standards.
| Standard | Detail |
|---|---|
| Everything in Git | Code, job and pipeline definitions, config, docs, tests |
| Branching | Short-lived branches from main, pull requests, reviews |
Protected main |
No direct pushes. Required checks and approvals |
| Commit messages | Say why. Link the ticket |
| CI on every pull request | Lint, unit tests, bundle validate, deploy to dev, integration tests |
| Build once, deploy many | The same wheel or bundle to every environment |
| Deploy as a service principal | Never a person's token |
| No manual edits in production | If it isn't in Git, it will be overwritten |
| Releases | Tags and semantic versions. Rollback = redeploy a tag |
| Secrets | In the CI secret store, never in the repository |
- Pre-commit hooks for format, lint and secret scanning
- Keep notebooks small and clean in Git: no outputs committed
- Detail: Modules 14, 15, 26
Production Readiness Checklist
By the end: check production readiness before launch.
Checklist. A pipeline is ready for production when each line is true:
Code
[ ] In Git, reviewed, tagged with a version
[ ] Unit tests pass. Integration test run on test data
[ ] No hard-coded paths, catalogs, secrets or user names
Data
[ ] Output grain, schema and meaning documented. Table owner set
[ ] Quality rules defined with actions. Quarantine in place
[ ] Idempotent: a rerun gives the same result. Backfill tested
Operations
[ ] Runs as a job with a service principal. Schedule and parameters set
[ ] Retries, timeouts and failure notifications set
[ ] Alerts for failure, duration and freshness
[ ] Runbook written. On-call named
Governance and cost
[ ] Permissions granted to groups, least privilege
[ ] Sensitive columns tagged and protected
[ ] Tags for cost attribution. Cost estimate known
Recovery
[ ] Time travel / restore plan tested
[ ] Rollback = previous tag redeployed. Tried once
- Run the list in a review meeting, not alone
- Keep it short enough to be used. Remove lines nobody checks
Anti-Patterns and Common Mistakes
By the end: recognise common anti-patterns.
| Anti-pattern | Why it hurts | Instead |
|---|---|---|
| One giant notebook | Can't test or reuse | Package and functions |
%run chains and copy-pasted notebooks |
Hidden dependencies | A package, imports |
| Hard-coded paths, catalogs, dates | Breaks between environments | Parameters and config |
inferSchema on production CSV/JSON |
Types change run to run | Explicit schemas, Auto Loader hints |
collect() / toPandas() on big data |
Driver out of memory | Keep it in Spark |
| Python UDFs for everything | Slow, no pushdown | Built-in functions, then pandas_udf |
coalesce(1) on large writes |
One slow task | Let the writer choose, OPTIMIZE later |
| Over-partitioning | Millions of tiny files | Liquid clustering |
mode("overwrite") on the whole table by habit |
Wipes everything | MERGE, replaceWhere |
| Appending without dedupe | Duplicates on retry | Idempotent writes |
| Swallowing exceptions | Silent bad data | Fail loudly, log the cause |
| Dropping bad rows silently | Hidden data loss | Quarantine with a reason |
| Production run by hand from a notebook | Not repeatable | Jobs from Git |
| A person's token in a job | Dies when they leave | Service principal |
| Secrets in code or widgets | Leaks | Secret scopes |
| Spark caching on serverless | Raises an error | Write to Delta |
| No monitoring until someone complains | Late, costly | Alerts on failure, freshness, cost |
Remember
Most of these feel fast on day one and cost weeks later. A standard is a decision made once.
What you can do now
- Framework and review lens, clean code, SOLID, modular design
- Quality, naming, schema and layout standards
- Observability, security, CI/CD standards
- The production readiness checklist and anti-patterns