Data Quality Framework
Quote
“Uncontrolled variation is the enemy of quality.”
— W. Edwards Deming, Out of the Crisis (1986)
“It is wrong to suppose that if you can’t measure it, you can’t manage it — a costly myth.”
— W. Edwards Deming, The New Economics (1993)
Summary
This note defines a full data quality framework for pipelines, organizing quality into six dimensions, mapping those checks onto medallion layers, and showing how tooling, quarantine, anomaly detection, orchestration, and SLAs combine to keep bad data from becoming trusted output.
Quality dimensions
- Defines completeness, uniqueness, validity, timeliness, accuracy, and consistency as the six recurring quality dimensions a pipeline must evaluate.
- Uses financial-data failure modes to show why each dimension needs its own detection logic rather than one generic quality score.
Medallion quality gates
- Maps bronze, silver, and gold to different gate priorities and failure responses, including halt, quarantine, advisory review, and publication blocking.
- Treats layer-specific quality policy as a core architecture choice rather than a later monitoring add-on.
Tooling, quarantine, and anomaly workflows
- Compares dbt tests, Great Expectations, Soda, custom SQL or Python checks, and Dataplex-quality scans, then explains quarantine design, replay, and anomaly detection for time-series data.
- Connects those tools to actual operational responses instead of stopping at test declaration syntax.
Operations and safety
- Warnings: silent row loss, duplicate drift, stale data, and cross-system inconsistency can all survive a technically successful pipeline run.
- Recommendations: define explicit gates per layer, surface freshness separately from pipeline success, quarantine bad rows with context, and wire quality checks into both orchestration and CI.
Glossary
Data quality dimension
A specific lens for evaluating whether data is fit for use, such as completeness or timeliness, rather than a vague overall impression of quality.
It matters here because the framework depends on separating different failure modes instead of collapsing them into one generic status.
Different dimensions fail differently
A dataset can be complete but inaccurate, or timely but inconsistent. Treating these as separate dimensions makes root-cause analysis much clearer.
Completeness
The extent to which all expected rows and required fields are present in a dataset.
It matters here because missing rows or null-heavy required columns are among the easiest ways for outputs to look plausible while being wrong.
Silent loss is dangerous
Completeness failures are especially costly when the pipeline still succeeds and downstream consumers have no reason to suspect missing records.
Uniqueness
The requirement that each logical entity appears only once at the intended grain.
It matters here because duplicates distort aggregates, joins, and derived metrics across every later stage.
Duplication compounds downstream
One duplicated row can influence multiple aggregates, derived features, and exports before anyone notices the original duplication event.
Validity
The degree to which values conform to allowed domains, structural rules, and business logic constraints.
It matters here because structurally present data is still unusable if it violates the business rules the platform depends on.
Rules must be executable
A business rule that lives only in documentation is not protecting anything. Validity improves only when the rule is turned into a real assertion.
Timeliness / freshness
The measure of whether data arrives or is updated within the expected time window for its consumers.
It matters here because pipelines that run on time can still publish stale or missing source data if freshness is not checked explicitly.
Success is not freshness
A green pipeline status can hide the fact that it processed yesterday’s file or missed the source’s expected update window entirely.
Accuracy
The degree to which values reflect the real-world truth they are supposed to represent.
It matters here because some of the most damaging pipeline failures are not structural at all; they are numerically wrong while still looking well formed.
Often needs an external reference
Accuracy is hard to automate because many checks require a second source, a reconciliation process, or anomaly thresholds rather than simple schema rules.
Consistency
The requirement that the same logical facts agree across systems, tables, and publications.
It matters here because conflicting values across channels destroy user trust faster than many other data defects.
Cross-system mismatch erodes confidence
Users rarely tolerate two authoritative answers to the same question. Consistency checks are what keep replication and publication steps credible.
Quality gate
A decision checkpoint that evaluates quality signals and either allows data to continue, quarantines part of it, or halts the pipeline.
It matters here because the note turns quality from passive monitoring into an active control over data movement.
Monitoring plus action
A gate is useful because it has consequences. It does not just record that quality is bad; it changes what the pipeline is allowed to do next.
Quarantine
A controlled holding area for bad or suspicious rows that should not continue downstream but still need to be preserved for investigation and replay.
It matters here because row-level defects should not force teams to choose between dropping evidence and stopping all processing blindly.
Preserve evidence and recover later
Quarantine gives teams a reversible response: protect downstream consumers now, then analyze, fix, and replay affected rows later.
Anomaly detection
A set of statistical or heuristic checks that flag unusual values or movements that may be valid but deserve investigation.
It matters here because some important quality failures appear as abnormal patterns rather than explicit rule violations.
Suspicious is not always wrong
Good anomaly detection should trigger review, not necessarily automatic failure. Real market or business events can produce legitimate outliers.
SLA
The documented commitment for freshness, availability, and acceptable quality levels of a dataset or pipeline output.
It matters here because quality priorities become operational only when the platform knows the deadline and thresholds it must enforce.
Turn quality into obligation
SLAs are what convert abstract quality expectations into something the platform can monitor, alert on, and escalate when breached.
Data Quality Dimensions
Completeness — all expected data is present
Completeness means every row and every required field that should exist actually exists. A dataset is incomplete when rows are missing entirely (e.g., a constituent dropped from the feed) or when required columns contain nulls.
Silent row loss is the hardest quality failure to detect
If your pipeline silently drops rows during ingestion (e.g., a malformed CSV line), downstream aggregates look plausible but are wrong. A 50-constituent index calculated from 49 prices is published before anyone notices.
Assert Row Counts Against Expected Baseline
After every ingestion load, compare the incoming row count against the prior-day count (or a source manifest if available). Fail the pipeline if the count falls below 80% of the prior load. Also assert
NOT NULLon all required columns before any rows leave bronze. Use a source manifest or API metadata endpoint to get the authoritative expected count when the source provides one.
How to detect: Compare incoming row counts against expected counts (prior day, reference dimension, source manifest). Assert non-null on required columns.
- Python row count assertion: 25_py_functional_pipeline > Polars — assert minimum row count with len()
- Full quality gate runner: 25_py_functional_pipeline > Pipeline — run all quality gate assertions with log.info()
- dbt row count tests: dbt-testing-framework > dbt-expectations — row count and statistical tests
- GCP row count monitoring: gcp-pipeline-health-and-sla > Row Count Validation
Uniqueness — no unwanted duplicates
Uniqueness means each entity appears exactly once per grain. Duplicate rows inflate aggregates, double-count weights, and corrupt joins.
Duplicate rows silently corrupt every downstream aggregate
A duplicated price row doubles a constituent’s weight contribution in a market-cap-weighted index. The index level shifts, and because the number looks reasonable, no human catches it until an investor reconciles against the exchange.
Assert Uniqueness on Natural Keys Before Every Transform
At the silver quality gate, assert that the natural key (instrument + trade_date, or equivalent) is unique before any aggregation or join runs. Use
dbt_utils.unique_combination_of_columnsin dbt and a Polarsdf.is_duplicated().any()assertion in Python. If duplicates exist, route the batch to quarantine and investigate the source before allowing any downstream run.
How to detect: Assert uniqueness on natural keys (instrument + trade_date). Hash-based dedup on composite keys.
- Python duplicate assertion: 25_py_functional_pipeline > Polars — assert no duplicate rows with unique()
- dbt composite uniqueness: dbt-testing-framework > dbt-utils test — unique_combination_of_columns
- dbt built-in unique/not_null: dbt-testing-framework > dbt Built-in Generic Tests
Validity — data conforms to business rules
Validity means values fall within acceptable domains and pass business logic rules. Prices must be positive. Weights must sum to 1.0. ESG scores must be 0-100.
Business rules that live only in someone's head are never enforced
If the rule “weights must sum to 1.0” is documented in a wiki but not coded as an assertion, it will eventually be violated. Encode every business rule as a testable assertion.
Encode Every Business Rule as a Tested Assertion
For each business rule, write a corresponding dbt custom test (a SQL query that returns rows on failure) or a Python assertion. Examples: a
tests/assert_weights_sum_to_100.sqlthat fails if any index’s weight deviates beyond 0.5%, and adbt_utils.expression_is_truecheck thatopen_price <= high_price. Store these tests in version control alongside the models they protect.
How to detect: Range checks, regex patterns, enum membership, cross-field logic (e.g., open ⇐ high, low ⇐ close).
- Python row-level validation: 25_py_functional_pipeline > Pydantic — validate Bronze rows with BaseModel() row-level check
- dbt expression assertions: dbt-testing-framework > dbt-utils test — expression_is_true
- dbt range checks: dbt-testing-framework > dbt-utils test — accepted_range
- Weight sum validation: pit-integrity-logic > Validation: Weight Sum Check
- Data contracts: dbt-data-contracts-implementation
Timeliness — data arrives within SLA
Timeliness means data is available when downstream consumers need it. Late data delays index publication, triggers SLA breaches, and may force fallback to stale values.
A pipeline that succeeds with stale data is worse than one that fails
If your pipeline runs on schedule but processes yesterday’s file because today’s hasn’t arrived, you publish stale index values with no alert. Always assert data freshness, not just pipeline completion.
Assert Data Freshness Separately from Pipeline Completion
After every load, assert that
MAX(trade_date)orMAX(loaded_at)in the target table is within the SLA window (e.g., today’s date for a daily pipeline). Use a dbt source freshness check in CI and a Pythonassert max_date >= today - timedelta(days=1)assertion in the production Airflow task. A dead man’s switch (an alert that fires if no successful load is recorded by the SLA deadline) catches the case where the pipeline never ran at all.
How to detect: Compare max timestamp in the dataset against expected freshness SLA. Implement dead man’s switch for expected-but-missing loads.
- Python freshness assertion: 25_py_functional_pipeline > Polars — assert data freshness against SLA with max()
- GCP freshness monitoring: gcp-pipeline-health-and-sla > Data Freshness Monitoring
- Dead man’s switch: gcp-pipeline-health-and-sla > Dead Man’s Switch (Heartbeat Monitoring)
Three Types of Freshness
Source freshness: when the source system last updated the data. Pipeline freshness: when the pipeline last successfully processed the data. Serving freshness: when the consumer last received updated data. A pipeline can be “fresh” (ran on time) while serving stale data (the source was late). Monitor all three independently.
Accuracy — data values are correct
Accuracy means recorded values match the real-world truth. A price of 150.00 is complete, unique, valid, and timely, but if the actual close was 151.00, it is inaccurate.
Accuracy is the hardest dimension to automate
You cannot validate accuracy without an independent reference source. For financial data, corroborate against a second vendor, an exchange API, or a manual check for high-impact values.
Use Statistical Anomaly Detection Plus Cross-Vendor Corroboration
Apply a rolling z-score (30-day window, 3-sigma threshold) to flag price and score values that deviate unusually from recent history. For high-impact values (index levels, published weights), cross-check against a second vendor or the exchange API as part of the gold quality gate. Flag anomalies for manual review rather than auto-failing — a genuine market event can produce a valid 5-sigma move.
How to detect: Cross-reference against independent sources. Statistical anomaly detection (z-score) to flag outliers for manual review. Reconciliation queries between systems.
- Python API corroboration: 25_py_functional_pipeline > yfinance — corroborate with live API data using Ticker.history()
- Regression snapshot comparison: data-pipeline-testing-strategy > Regression tests — snapshot comparison
Consistency — data agrees across systems
Consistency means the same logical entity has the same value in every system that stores it. SQL Server gold must match BigQuery published values. Dimension attributes must agree across fact tables.
Cross-system inconsistency erodes trust faster than any other quality failure
When a client sees one index level on a website and a different level in a downloaded file, they lose confidence in all your data, even the parts that are correct.
Run Cross-System Reconciliation Queries After Every Publication
After each export from SQL Server gold to BigQuery, run a reconciliation query comparing
COUNT(*),SUM(index_level), andMIN/MAX(trade_date)between both systems. Hash the entire gold table usinghashlib.sha256on a sorted export and store the hash as a pipeline artifact. Any mismatch halts publication until the source of the discrepancy is identified and resolved.
How to detect: Reconciliation queries comparing row counts, checksums, and key aggregates across systems. Hash-based comparison of entire datasets.
- Python deterministic hash: 25_py_functional_pipeline > hashlib — compute deterministic DataFrame hash with sha256()
- SCD Type 2 consistency: silver-transforms > silver.index_dim — SCD Type 2 Dimension
- Upsert consistency: sql-server-loading-patterns > Upsert (INSERT + UPDATE)
Quality Gates by Medallion Layer
Each medallion-architecture layer has different quality priorities. Bronze gates protect ingestion integrity. Silver gates enforce business rules. Gold gates guard publication correctness.
Bronze Quality Gate
Bronze (medallion-architecture > Bronze (Raw)) validates that raw data landed correctly before any transformation.
Tier 1 — Critical (halt pipeline)
- Schema conformance: column names and types match expected contract
- File hash verification: SHA-256 matches source manifest
- Zero-byte / empty file detection
Fix: Halt and Quarantine on Tier 1 Failure
If schema conformance fails, do not load any rows — raise a
SchemaDriftErrorand quarantine the entire file. Verify the SHA-256 hash against the source manifest before opening the file. Detect zero-byte files with a pre-check (os.path.getsize) and alert immediately. Resume only after the upstream issue is confirmed resolved.
Tier 2 — Standard (quarantine bad rows)
- Null rate exceeds threshold on required fields (>5%)
- Duplicate detection on natural keys
- Malformed rows (parse failures, encoding errors)
Fix: Separate Good and Bad Rows, Continue with Good
Split the batch into passing and failing rows. Write failing rows to the quarantine table with
failure_reason,gate_name, andrejected_atmetadata. Load the passing rows downstream so the pipeline continues without the bad rows. Alert the data team and set areplayed_attarget of < 24 hours.
Tier 3 — Advisory (log for review)
- Row count outside 80-120% of prior load
- New enum values not in reference table
- Column order changed (schema evolution signal)
- Bronze gate implementation: 25_py_functional_pipeline > Pipeline — run Bronze data quality gate with run_quality_gate()
- Quality gate pattern: functional-pipeline-architecture > Quality Gate Pattern
- Data quality assertions: data-pipeline-testing-strategy > Data quality assertions
Silver Quality Gate
Silver (medallion-architecture > Silver (Cleaned)) enforces business rules and referential integrity on cleaned data.
Tier 1 — Critical (halt pipeline)
- Business rule violation on mandatory fields (price ⇐ 0, negative volume)
- Referential integrity failure (instrument not in dimension table)
- Deduplication check fails after cleaning
Fix: Halt Silver Pipeline on Critical Business Rule Failures
If any price ≤ 0 or volume < 0 rows survive bronze, halt the silver pipeline and quarantine the affected instruments. Validate referential integrity with a
LEFT JOINagainst the dimension table before any fact transform — reject rows with no matching dimension entry. Re-run deduplication and assert uniqueness before promoting to silver; if duplicates persist, investigate the bronze load strategy.
Tier 2 — Standard (quarantine bad rows)
- Values outside expected statistical range (z-score > 3)
- Staleness: data older than freshness SLA
- Cross-field logic violations (open > high, low > close)
Fix: Quarantine Outliers and Cross-Field Violations
Compute a 30-day rolling z-score per instrument; quarantine rows where
|z| > 3for manual review. Assertopen_price <= high_price AND low_price <= close_pricewith a dbtexpression_is_truetest. For stale rows, checkMAX(trade_date)against the SLA before running transforms — fall back to T-1 values only when explicitly configured, and log every fallback occurrence.
Tier 3 — Advisory (log for review)
- Rows requiring fallback to T-1 values
- Minor schema drift (new nullable columns)
- Data distribution shift beyond 1 standard deviation
What Is Schema Drift?
Schema drift occurs when a data source changes its schema without notice — a column is renamed, a type changes, a new field appears, or a field disappears. The pipeline’s contract expects the OLD schema. The source delivers the NEW schema. Without detection, the pipeline silently loads NULLs (renamed column), fails mid-transform (type change), or ignores new data (unknown column). Detection methods: compare incoming columns against the contract, hash the schema, alert on mismatch.
- Silver gate implementation: 25_py_functional_pipeline > Pipeline — run Silver data quality gate with run_quality_gate()
- Row-level Pydantic validation: 25_py_functional_pipeline > Pydantic — validate Bronze rows with BaseModel() row-level check
- dbt test severity: dbt-testing-framework > dbt Test severity: warn vs error
Gold Quality Gate
Gold (medallion-architecture > Gold (Analytics)) is the last line of defense before data reaches clients, regulatory filings, and downstream systems.
Gold is publication — treat every Gold check as a circuit breaker
If a quality gate at the Gold layer fails and the pipeline continues anyway (e.g., because the check was set to
severity: warninstead oferror), incorrect index values reach clients and regulatory filings. Gold-layer checks that affect publication integrity must ALWAYS halt the pipeline. See esg circuit breaker fired for a real incident where this saved us.
Set All Gold-Layer dbt Tests to severity: error
In every gold model’s
schema.yml, setconfig: severity: erroron every test — notwarn. Use aShortCircuitOperatorin the Airflow DAG afterdbt testso that any single gold test failure halts publication immediately. Never route gold-layer quality failures to a log-and-continue path.
Tier 1 — Critical (halt publication)
- Weights sum to 1.0:
ABS(SUM(weight) - 1.0) < 1e-9- No missing constituents: count matches target (e.g., 50)
- Index level sanity: daily change within +/-15%
- Cross-dataset consistency: SQL Server gold = BigQuery published
Fix: Implement Weight and Constituent Circuit Breakers
Assert
ABS(SUM(weight_pct) - 1.0) < 1e-9perindex_code + trade_datein a custom dbt test. AssertCOUNT(DISTINCT instrument_isin) = target_countfor each index. Compare the calculated index level against the prior day’s level and reject if the daily change exceeds ±15%. Cross-check SQL Server gold vs BigQuery row count and checksum before publishing to any downstream consumer.
Tier 2 — Standard (quarantine and alert)
- ESG score outside normalized 0-100 range
- Sector allocation drift beyond threshold
- Turnover exceeds rebalance limits
Fix: Alert and Hold on Tier 2 Gold Failures
For ESG scores outside 0–100, quarantine the affected instruments and send an immediate alert to the data team — do not publish affected scores until reviewed. For sector drift and turnover threshold breaches, compare against the prior rebalance snapshot and hold publication pending a manual sign-off from the index operations team.
Tier 3 — Advisory (log for review)
- Minor rounding differences across systems (<1e-6)
- Constituent weight below minimum threshold
- Publication timestamp later than typical
- Weight validation: pit-integrity-logic > Validation: Weight Sum Check
- Circuit breaker incident: esg circuit breaker fired
- Store test failures for audit: dbt-testing-framework > dbt —store-failures
Data Quality Tooling
dbt Tests — SQL assertions in YAML
Best for: Schema validation, business rule checks, referential integrity in warehouse-centric pipelines.
Strengths: Version-controlled with models, runs in CI/CD, built-in severity levels, test failure storage for audit.
Limitations: SQL-only (no Python logic), limited statistical capability without dbt-expectations, test runs add warehouse cost.
Anti-pattern: writing dbt tests that duplicate warehouse constraints
If your warehouse enforces NOT NULL and UNIQUE via DDL constraints, dbt tests on the same columns are redundant cost. Use dbt tests for business rules the warehouse cannot enforce.
Reserve dbt Tests for Business Logic the Warehouse Cannot Enforce
Audit your
schema.ymlfornot_nullanduniquetests on columns already covered by DDLNOT NULLandUNIQUEconstraints. Remove the duplicates. Instead, invest dbt test slots in cross-field logic (expression_is_true), range checks (accepted_range), referential integrity (relationships), and aggregate rules (weight sums, constituent counts) that no DDL constraint can express.
- Built-in generic tests: dbt-testing-framework > dbt Built-in Generic Tests
- Statistical tests: dbt-testing-framework > dbt-expectations — row count and statistical tests
- CI/CD integration: data-pipeline-testing-strategy > CI/CD Test Automation
Great Expectations — Python assertion suites with profiling
Best for: Statistical profiling, data documentation, complex multi-column expectations that are awkward in SQL.
Strengths: Rich expectation library, auto-profiling to bootstrap suites, data docs for stakeholder visibility.
Limitations: Heavy dependency footprint, slower than native SQL checks, checkpoint configuration complexity.
Anti-pattern: auto-profiling in production without review
Great Expectations can auto-generate expectations from data. If you deploy auto-profiled suites without human review, you encode current data quirks as rules — including bugs. Always review and curate generated expectations.
Treat Auto-Profiled Suites as a Draft, Not a Final Rule Set
Run
great_expectations suite new --profileagainst a representative data sample to generate the initial expectation suite, then manually review every generated expectation before committing it to the repository. Remove expectations that reflect current bugs or data anomalies. For each expectation kept, add a comment explaining the business reason so future reviewers understand its intent.
Soda Core — YAML-defined checks with SodaCL
Best for: Quick setup across multiple data sources, team-friendly YAML syntax, Soda Cloud dashboards for non-technical stakeholders.
Strengths: Multi-source (SQL, Spark, Pandas), SodaCL is readable by analysts, anomaly detection built in.
Limitations: Advanced checks require Soda Cloud (paid), fewer community extensions than dbt or GE.
Custom SQL / Python — stored procedures and scripts
Best for: Legacy systems, highly specific edge cases, environments where adding a framework is impractical.
Strengths: Full flexibility, no dependency overhead, can run in any environment.
Limitations: No standardization, no built-in reporting, maintenance burden grows with pipeline count.
- Python quality gate: 25_py_functional_pipeline > Pipeline — run all quality gate assertions with log.info()
- C# functional pipeline: 25_cs_functional_pipeline
Dataplex Quality (GCP) — native BigQuery quality scans
Best for: BigQuery-centric pipelines on GCP, teams already using Dataplex for data governance.
Strengths: Zero infrastructure to manage, native BigQuery integration, results in Cloud Monitoring.
Limitations: GCP-only, limited custom logic, no cross-cloud support.
- GCP pipeline health: gcp-pipeline-health-and-sla > Row Count Validation
When to Combine Tools
No single tool covers all six quality dimensions. A practical stack for financial pipelines:
| Layer | Tool | Covers |
|---|---|---|
| Bronze ingestion | Custom Python (Pydantic + Polars) | Completeness, Validity, Uniqueness |
| Silver / Gold warehouse | dbt tests + dbt-expectations | All six dimensions in SQL |
| Cross-system reconciliation | Custom SQL / Python | Consistency, Accuracy |
| Monitoring and alerting | GCP Dataplex + Cloud Monitoring | Timeliness, Completeness |
Start with dbt tests and custom Python gates, then add Great Expectations or Soda only when you need statistical profiling or multi-source checks that justify the extra dependency.
The Quarantine Pattern
What Is a Quarantine?
A quarantine isolates rows that fail quality checks so they can be investigated and replayed without blocking the pipeline. Good rows continue downstream; bad rows are persisted with failure metadata.
Never silently drop bad rows
Dropping rows that fail validation means you lose evidence of upstream data issues. Quarantined rows are your forensic trail: they tell you what went wrong, when, and how often. Without quarantine, you discover data loss only when a client reports it.
Always Route Rejected Rows to the Quarantine Table
In every quality gate, split the batch into passing and failing rows using a Polars filter or a SQL
CASEexpression. Insert failing rows intobronze.quarantinewithsource_table,source_row_json,failure_reason,gate_name, andrejected_at. Never useDROP,DELETE, or silent filtering — every rejected row must be traceable and replayable.
- Quarantine pattern overview: functional-pipeline-architecture > The Quarantine Pattern
- Dead letter queue (same concept, different name): error-handling-and-retry-patterns > Dead Letter Queue (DLQ) — don’t drop, don’t retry forever
Quarantine Table Design
Every quarantine table needs the original row, the failure reason, and enough metadata to replay.
-- Quarantine table DDL — one per source or shared across Bronze
CREATE TABLE bronze.quarantine (
quarantine_id INT IDENTITY(1,1) PRIMARY KEY,
source_table NVARCHAR(128) NOT NULL,
source_row_json NVARCHAR(MAX) NOT NULL,
failure_reason NVARCHAR(512) NOT NULL,
gate_name NVARCHAR(128) NOT NULL,
rejected_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
replayed_at DATETIME2 NULL
);- Python quarantine table creation: 25_py_functional_pipeline > SQL Server — create quarantine table for rejected rows with cursor.execute()
- Python quarantine persistence: 25_py_functional_pipeline > SQL Server — define quarantine persistence helper with cursor.execute()
Reject-Persist-Investigate Workflow
- Reject — Quality gate identifies failing rows and separates them from the good batch
- Persist — Failing rows are written to the quarantine table with failure metadata (reason, gate name, timestamp)
- Investigate — Data engineers query the quarantine table to diagnose root cause (bad source, schema drift, business rule change)
- Fix — Correct the upstream issue or update the validation rule
- Replay — Re-ingest corrected rows through the pipeline; mark
replayed_atin quarantine
Quarantine Metrics
Track these metrics to measure data quality over time:
- Quarantine rate — percentage of rows rejected per load (target: <1% for Bronze, <0.1% for Silver)
- Mean time to replay — how long quarantined rows sit before investigation (target: <24 hours)
- Repeat offenders — source_table + failure_reason combinations that recur (signals upstream fix needed)
- Quarantine growth — if the table grows faster than replays, investigation is falling behind
Replay Pattern
Replaying quarantined rows must be idempotent. The replay process re-ingests rows through the same pipeline (not a direct insert into Silver/Gold) so all quality gates run again.
Mark replayed rows, don't delete them
Set
replayed_aton successfully replayed rows instead of deleting them. The quarantine table is an audit log; deleting rows destroys the quality history.
Anomaly Detection for Financial Time Series
Statistical anomaly detection catches data issues that pass business rule validation but are still wrong (e.g., a price that is positive and within range but 10x the prior day’s close).
Z-score formula:
# Rolling z-score for anomaly detection
z_score = (value - rolling_mean) / rolling_std
is_anomaly = abs(z_score) > thresholdWindow size guidance:
| Data type | Window | Threshold | Rationale |
|---|---|---|---|
| Prices (daily close) | 30 trading days | 3 sigma | Prices are relatively stable; 3 sigma catches true outliers |
| Volumes (daily) | 90 trading days | 2 sigma | Volumes are noisier; 2 sigma with longer window smooths seasonal patterns |
| Row counts (per load) | 30 loads | 2 sigma | Catches ingestion anomalies without over-alerting |
Seasonal adjustment is critical for volume data
Trading volumes spike predictably around index rebalance dates, options expiry, and quarter-end. A naive z-score flags every predictable spike as anomalous. Either exclude known event dates from the rolling window or use a seasonal decomposition model.
Exclude Known Event Dates from the Rolling Window
Maintain a reference table of known high-volume event dates (quarterly rebalance dates, options expiry dates, index reconstitution dates). When computing the rolling z-score for volume anomaly detection, filter out these dates from the lookback window using a
LEFT JOINanti-pattern:WHERE trade_date NOT IN (SELECT event_date FROM ref.known_volume_events). This eliminates false positives on predictable spikes while retaining sensitivity to genuine anomalies.
Quality Gate Orchestration
Airflow Integration — ShortCircuitOperator as quality gate
Use Airflow’s ShortCircuitOperator to implement quality gates as pipeline tasks. If critical checks fail, the operator returns False and skips all downstream tasks, preventing bad data from reaching publication.
# Airflow ShortCircuitOperator quality gate
quality_gate = ShortCircuitOperator(
task_id="bronze_quality_gate",
python_callable=run_quality_checks,
)ShortCircuitOperator vs BranchPythonOperator
Use
ShortCircuitOperatorwhen failure means “stop everything.” UseBranchPythonOperatorwhen failure means “take an alternate path” (e.g., quarantine and continue with good rows).
- Airflow DAG patterns: airflow-dag-patterns
GitHub Actions Integration — dbt test in CI/CD
Run dbt test --select state:modified+ on every pull request to catch quality regressions before they reach production. Only modified models and their downstream dependents are tested, keeping CI fast.
# GitHub Actions step for dbt quality checks
- run: dbt test --select state:modified+ --defer --state prod-manifest/- CI/CD test automation: data-pipeline-testing-strategy > CI/CD Test Automation
SLA Definitions by Dataset
| Dataset | Freshness SLA | Quality Threshold | Fallback |
|---|---|---|---|
| Market data (OHLCV) | T+0 by 18:30 UTC | 100% completeness | Exchange backup feed |
| ESG scores | T+0 by Monday 08:00 UTC | 95% coverage | Use T-1 scores |
| Corporate actions | T-1 by 06:00 UTC | 100% mandatory actions | Manual sourcing |
| Index levels (published) | T+0 by 19:00 UTC | 100% accuracy | Hold publication |
| Reference data (dimensions) | T-1 by 04:00 UTC | 100% completeness | Use prior version |
SLAs are per-dataset, not per-pipeline
A single pipeline may load multiple datasets with different SLAs. Define freshness and quality thresholds at the dataset level, then map pipeline tasks to the strictest SLA they serve.
Related
- data-pipeline-testing-strategy — Testing pyramid that coordinates quality checks with unit, integration, and contract tests
- data-contracts — Schema and SLA agreements between producers and consumers
- error-handling-and-retry-patterns — Retry logic, dead letter queues, and circuit breakers
- functional-pipeline-architecture — Quality gate and quarantine patterns in functional style
- medallion-architecture — Bronze / Silver / Gold layer definitions and responsibilities
- observability-strategy-matrix — Logging, metrics, and alerting strategy across pipeline layers
- 25_py_functional_pipeline — Full Python implementation of quality gates, quarantine, and anomaly detection
- 25_cs_functional_pipeline — C# implementation of the same patterns
- dbt-testing-framework — dbt test types, severity levels, and store-failures
- gcp-pipeline-health-and-sla — GCP-native freshness monitoring and alerting