<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bobby Ray</title>
    <description>The latest articles on DEV Community by Bobby Ray (@bobby_ray_581732c715283b2).</description>
    <link>https://dev.to/bobby_ray_581732c715283b2</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4013610%2F13f8322b-74aa-4d05-94bd-a820a1e5c033.jpg</url>
      <title>DEV Community: Bobby Ray</title>
      <link>https://dev.to/bobby_ray_581732c715283b2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bobby_ray_581732c715283b2"/>
    <language>en</language>
    <item>
      <title>What Should a Real Data Engineer Assessment Actually Test?</title>
      <dc:creator>Bobby Ray</dc:creator>
      <pubDate>Thu, 27 Aug 2026 23:26:31 +0000</pubDate>
      <link>https://dev.to/bobby_ray_581732c715283b2/what-should-a-real-data-engineer-assessment-actually-test-5aj4</link>
      <guid>https://dev.to/bobby_ray_581732c715283b2/what-should-a-real-data-engineer-assessment-actually-test-5aj4</guid>
      <description>&lt;p&gt;What Should a# What Should a Real Data Engineer Assessment Actually Test?&lt;/p&gt;

&lt;p&gt;Data Engineering interviews are surprisingly inconsistent.&lt;/p&gt;

&lt;p&gt;One company may focus heavily on SQL. Another may ask Python coding questions. Another may go deep on Spark, data modeling, cloud architecture, or distributed systems. And sometimes candidates still get generic algorithm questions that have little connection to the work they would actually do.&lt;/p&gt;

&lt;p&gt;That raises a better question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you had 60–90 minutes to evaluate a Data Engineer, what would actually be worth testing?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I think the answer should be much closer to real engineering work.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. SQL reasoning, not SQL trivia
&lt;/h2&gt;

&lt;p&gt;A useful assessment can test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;joins and cardinality&lt;/li&gt;
&lt;li&gt;window functions&lt;/li&gt;
&lt;li&gt;aggregations&lt;/li&gt;
&lt;li&gt;duplicate handling&lt;/li&gt;
&lt;li&gt;null behavior&lt;/li&gt;
&lt;li&gt;query correctness&lt;/li&gt;
&lt;li&gt;performance trade-offs&lt;/li&gt;
&lt;li&gt;debugging incorrect results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What is the syntax for &lt;code&gt;ROW_NUMBER()&lt;/code&gt;?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;give the candidate a realistic dataset and ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We need the most recent valid record for each customer. The source contains duplicates and late-arriving updates. How would you solve it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That tells you much more.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Data modeling
&lt;/h2&gt;

&lt;p&gt;Useful questions should involve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;grain&lt;/li&gt;
&lt;li&gt;facts and dimensions&lt;/li&gt;
&lt;li&gt;slowly changing dimensions&lt;/li&gt;
&lt;li&gt;normalization vs. denormalization&lt;/li&gt;
&lt;li&gt;analytical access patterns&lt;/li&gt;
&lt;li&gt;historical correctness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Define SCD Type 2.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;try:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The business needs to report revenue using the customer segment that was valid when the transaction happened. How would you model that?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now the candidate has to reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pipeline reliability
&lt;/h2&gt;

&lt;p&gt;Production pipelines fail.&lt;/p&gt;

&lt;p&gt;A practical assessment should test whether the candidate understands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;idempotency&lt;/li&gt;
&lt;li&gt;late-arriving data&lt;/li&gt;
&lt;li&gt;duplicate events&lt;/li&gt;
&lt;li&gt;backfills&lt;/li&gt;
&lt;li&gt;partial failures&lt;/li&gt;
&lt;li&gt;schema changes&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;li&gt;data quality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A candidate who can build a pipeline but cannot reason about failure modes is not finished yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Python in a Data Engineering context
&lt;/h2&gt;

&lt;p&gt;Python matters, but the problem should match the role.&lt;/p&gt;

&lt;p&gt;Useful areas include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;parsing&lt;/li&gt;
&lt;li&gt;transformation&lt;/li&gt;
&lt;li&gt;validation&lt;/li&gt;
&lt;li&gt;APIs&lt;/li&gt;
&lt;li&gt;file processing&lt;/li&gt;
&lt;li&gt;testing&lt;/li&gt;
&lt;li&gt;error handling&lt;/li&gt;
&lt;li&gt;memory/performance reasoning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For many Data Engineering roles, this is more useful than an unrelated algorithm puzzle.&lt;/p&gt;

&lt;p&gt;The question should be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can this person write maintainable data-processing code?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did this person memorize the exact interview pattern we selected?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  5. Engineering judgment
&lt;/h2&gt;

&lt;p&gt;Give the candidate an imperfect system.&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What would you change first?&lt;/li&gt;
&lt;li&gt;What assumptions worry you?&lt;/li&gt;
&lt;li&gt;Where would you add monitoring?&lt;/li&gt;
&lt;li&gt;What would fail at 10× the data volume?&lt;/li&gt;
&lt;li&gt;Which parts would you keep simple?&lt;/li&gt;
&lt;li&gt;What would you not optimize yet?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is rarely one perfect answer.&lt;/p&gt;

&lt;p&gt;That is the point.&lt;/p&gt;

&lt;p&gt;Strong engineers explain trade-offs.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Communication
&lt;/h2&gt;

&lt;p&gt;A Data Engineer needs to explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;why a model was chosen&lt;/li&gt;
&lt;li&gt;why a pipeline is failing&lt;/li&gt;
&lt;li&gt;what a data-quality issue means&lt;/li&gt;
&lt;li&gt;which trade-offs matter&lt;/li&gt;
&lt;li&gt;what should be fixed first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would rather see a clear, defensible decision than a technically clever solution nobody can maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Respect the candidate's time
&lt;/h2&gt;

&lt;p&gt;Candidates should know before starting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;estimated time&lt;/li&gt;
&lt;li&gt;format&lt;/li&gt;
&lt;li&gt;what skills are being evaluated&lt;/li&gt;
&lt;li&gt;whether work can be saved&lt;/li&gt;
&lt;li&gt;whether they can resume later&lt;/li&gt;
&lt;li&gt;what happens after submission&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Assessment difficulty can be meaningful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assessment access should not be difficult.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A better principle
&lt;/h2&gt;

&lt;p&gt;A good Data Engineering assessment should answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can this person reason through realistic Data Engineering problems?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It should not primarily answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can this person optimize for our interview format?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What I am experimenting with
&lt;/h2&gt;

&lt;p&gt;I've been working with &lt;strong&gt;Anzibloom&lt;/strong&gt;, a technical hiring platform experimenting with role-specific assessment before employer introductions.&lt;/p&gt;

&lt;p&gt;The idea is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Practical assessment
        ↓
Verified technical evidence
        ↓
Relevant opportunities
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is not to make assessment easier.&lt;/p&gt;

&lt;p&gt;It is to make the assessment more representative of the work—and make the resulting evidence more useful than a disposable screening test.&lt;/p&gt;

&lt;p&gt;If you work in Data Engineering, I would especially like your criticism:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What would you include in a 60–90 minute Data Engineer assessment, and what would make you stop halfway through one?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want to see the approach in practice, you can check out the Data Engineering assessment on &lt;a href="//www.anzibloom.com"&gt;Anzibloom&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>anzibloom</category>
      <category>dataengineering</category>
      <category>career</category>
      <category>interviewing</category>
    </item>
    <item>
      <title>Contract Versioning in Production Pipelines: Registry, CLI, and Run History</title>
      <dc:creator>Bobby Ray</dc:creator>
      <pubDate>Thu, 27 Aug 2026 08:51:55 +0000</pubDate>
      <link>https://dev.to/bobby_ray_581732c715283b2/contract-versioning-in-production-pipelines-registry-cli-and-run-history-13el</link>
      <guid>https://dev.to/bobby_ray_581732c715283b2/contract-versioning-in-production-pipelines-registry-cli-and-run-history-13el</guid>
      <description>&lt;p&gt;Dataset contracts are easy to write once. They are hard to &lt;strong&gt;operate&lt;/strong&gt; when you cannot answer: which version failed, who pinned it, and whether the registry agrees with the YAML on disk.&lt;/p&gt;

&lt;p&gt;This article closes the loop on &lt;a href="https://dev.to/bobby_ray_581732c715283b2/data-quality-contracts-in-production-pipelines-without-a-separate-platform-team-f3"&gt;Data Quality Contracts in Production Pipelines&lt;/a&gt; â€” registry â†’ CLI resolution â†’ Airflow scheduling â†’ versioned run history, with ingestion quarantine in &lt;a href="https://github.com/br413/production-data-pipeline" rel="noopener noreferrer"&gt;production-data-pipeline&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a href="https://br413.github.io/" rel="noopener noreferrer"&gt;br413.github.io&lt;/a&gt; Â· &lt;strong&gt;Quality layer:&lt;/strong&gt; &lt;a href="https://github.com/br413/data-quality-observability" rel="noopener noreferrer"&gt;data-quality-observability&lt;/a&gt; Â· &lt;strong&gt;Ingestion:&lt;/strong&gt; &lt;a href="https://github.com/br413/production-data-pipeline/releases/tag/v0.2.1" rel="noopener noreferrer"&gt;production-data-pipeline v0.2.1&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why versioning matters at the dataset boundary
&lt;/h2&gt;

&lt;p&gt;Row-level quarantine catches poison pills during API ingestion. Dataset contracts catch &lt;strong&gt;schema drift, stale facts, and broken foreign keys&lt;/strong&gt; before promote.&lt;/p&gt;

&lt;p&gt;Without versioning you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A contract file changes; scheduled runs still report failures against an unknown baseline&lt;/li&gt;
&lt;li&gt;On-call cannot tell whether freshness regressed on &lt;strong&gt;v1.0&lt;/strong&gt; or &lt;strong&gt;v1.1&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Reviewers cannot see breaking changes without diffing YAML by hand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A lightweight registry fixes discoverability and pins â€” without standing up Confluent Schema Registry for a portfolio-scale stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: File-based registry
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;contracts/registry.yml&lt;/code&gt; is the canonical catalog:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;contracts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;orders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0"&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders.yml&lt;/span&gt;
  &lt;span class="na"&gt;customers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0"&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customers.yml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Semver policy (PATCH / MINOR / MAJOR) lives in &lt;a href="https://github.com/br413/data-quality-observability/blob/main/docs/adr/0002-schema-registry-and-contract-versioning.md" rel="noopener noreferrer"&gt;ADR 0002&lt;/a&gt;. Breaking changes require:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Major bump in the contract YAML&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;current&lt;/code&gt; update in the &lt;strong&gt;same PR&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Entry in &lt;code&gt;contracts/CHANGELOG.md&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Layer 2: CLI resolves by name
&lt;/h2&gt;

&lt;p&gt;Operators no longer pass file paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; src.dqo.cli run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--contract&lt;/span&gt; orders &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; data/samples/orders.csv &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--references&lt;/span&gt; data/samples
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI loads &lt;code&gt;registry.yml&lt;/code&gt;, resolves &lt;code&gt;orders.yml&lt;/code&gt;, and runs schema / null / freshness / RI checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: Airflow uses registry names
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;dqo_contract_checks&lt;/code&gt; DAG runs registry-backed tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run_orders_checks    â†’ --contract orders
run_customers_checks â†’ --contract customers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optional &lt;code&gt;DQO_WEBHOOK_URL&lt;/code&gt; routes contract failures the same way ingestion alerts work in the pipeline repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: Versioned run history
&lt;/h2&gt;

&lt;p&gt;Each run persists &lt;strong&gt;which contract version was evaluated&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-07-14T10:00:00+00:00  run-id  v1.0  passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On-call can correlate freshness regressions to a semver pin instead of guessing from git history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 5: Cross-repo pins from ingestion
&lt;/h2&gt;

&lt;p&gt;Ingestion quarantine and dataset contracts are separate boundaries. &lt;code&gt;production-data-pipeline&lt;/code&gt; declares pins in config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;pins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;orders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders@1.0&lt;/span&gt;
  &lt;span class="na"&gt;customers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customers@1.0&lt;/span&gt;

&lt;span class="na"&gt;dqo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;project_root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../data-quality-observability&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the full-stack demo config-driven â€” not buried in a README footnote.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI guards (the part teams skip)
&lt;/h2&gt;

&lt;p&gt;Registry consistency runs in CI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every registry path exists on disk&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;current&lt;/code&gt; matches the contract YAML &lt;code&gt;version&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CHANGELOG documents the active version&lt;/li&gt;
&lt;li&gt;PRs that bump &lt;code&gt;version&lt;/code&gt; must update registry &lt;strong&gt;and&lt;/strong&gt; CHANGELOG
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python scripts/validate_registry.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skipping this step is how "silent schema drift" returns â€” with extra YAML files.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this fits the portfolio stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;production-data-pipeline (ingestion + quarantine + contract pins)
    â†” data-quality-observability (registry + CLI + history)
    â†” Airflow DAGs (scheduled checks)
    â†” Dev.to series (public narrative)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer answers a different reviewer question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quarantine&lt;/strong&gt; â€” Can you isolate bad rows without aborting the batch?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contracts&lt;/strong&gt; â€” Can you define dataset quality explicitly?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioning&lt;/strong&gt; â€” Can you operate those contracts over time?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would add next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Export landed tables to CSV/Parquet and wire dqo into the pipeline Airflow DAG after dbt&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;registry_revision&lt;/code&gt; (git SHA) in run history for full audit trails&lt;/li&gt;
&lt;li&gt;One upstream merge on Airflow or dbt docs to balance portfolio depth with OSS signal&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related writing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/bobby_ray_581732c715283b2/building-a-production-data-pipeline-with-incremental-loading-and-dbt-2e2c"&gt;Building a Production Data Pipeline with Incremental Loading and dbt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/bobby_ray_581732c715283b2/data-quality-contracts-in-production-pipelines-without-a-separate-platform-team-f3"&gt;Data Quality Contracts in Production Pipelines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://br413.github.io/" rel="noopener noreferrer"&gt;Portfolio site&lt;/a&gt; Â· &lt;a href="https://github.com/br413" rel="noopener noreferrer"&gt;GitHub profile&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you operate contract versioning differently â€” Glue Registry, Data Contract CLI, or dbt exposures only â€” I am interested in how you draw the boundary between row-level and dataset-level gates.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>python</category>
      <category>dataquality</category>
      <category>airflow</category>
    </item>
    <item>
      <title>What I Learned Contributing to Prefect, dbt, and Airflow (An Honest OSS Retrospective)</title>
      <dc:creator>Bobby Ray</dc:creator>
      <pubDate>Fri, 21 Aug 2026 20:45:26 +0000</pubDate>
      <link>https://dev.to/bobby_ray_581732c715283b2/what-i-learned-contributing-to-prefect-dbt-and-airflow-an-honest-oss-retrospective-1ki8</link>
      <guid>https://dev.to/bobby_ray_581732c715283b2/what-i-learned-contributing-to-prefect-dbt-and-airflow-an-honest-oss-retrospective-1ki8</guid>
      <description>&lt;p&gt;Portfolio repos prove you can build. &lt;strong&gt;Upstream merges&lt;/strong&gt; prove you can collaborate with teams that maintain the tools production platforms run on. Over roughly ninety days I ran both tracks in parallel â€” portfolio releases, Dev.to writing, and OSS contributions to Prefect, dbt docs, Airflow, and Meltano â€” without backdating history or republishing private employer work.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a href="https://br413.github.io/" rel="noopener noreferrer"&gt;br413.github.io&lt;/a&gt; Â· &lt;strong&gt;90-day plan:&lt;/strong&gt; &lt;a href="https://github.com/br413/br413/blob/main/docs/90-day-contribution-plan.md" rel="noopener noreferrer"&gt;github.com/br413/br413&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why upstream, not just portfolio
&lt;/h2&gt;

&lt;p&gt;A strong GitHub profile needs more than greenfield demos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hiring signal&lt;/strong&gt; â€” judgment inside someone else's codebase, not only your own repo boundaries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational credibility&lt;/strong&gt; â€” fixes that reflect how platforms fail at 2 AM, not tutorial happy paths&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collaboration proof&lt;/strong&gt; â€” you can respond to review feedback and respect maintainer direction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My portfolio stack â€” &lt;a href="https://github.com/br413/production-data-pipeline" rel="noopener noreferrer"&gt;production-data-pipeline&lt;/a&gt;, &lt;a href="https://github.com/br413/data-quality-observability" rel="noopener noreferrer"&gt;data-quality-observability&lt;/a&gt;, &lt;a href="https://github.com/br413/lakehouse-platform-starter" rel="noopener noreferrer"&gt;lakehouse-platform-starter&lt;/a&gt; â€” gave &lt;strong&gt;real context&lt;/strong&gt; for what to fix upstream. The rule I followed: &lt;strong&gt;comment on the issue before opening the PR.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What merged (and why those landed)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PR&lt;/th&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Why it merged&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/PrefectHQ/prefect/pull/22500" rel="noopener noreferrer"&gt;Prefect #22500&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Prefect&lt;/td&gt;
&lt;td&gt;Kubernetes readiness vs liveness probes&lt;/td&gt;
&lt;td&gt;Small, verifiable ops detail; maintainer-aligned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/dbt-labs/docs.getdbt.com/pull/9606" rel="noopener noreferrer"&gt;dbt docs #9606&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;dbt docs&lt;/td&gt;
&lt;td&gt;Prefixed custom schema troubleshooting&lt;/td&gt;
&lt;td&gt;Deployment pitfall many teams hit silently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/apache/airflow/pull/71158" rel="noopener noreferrer"&gt;Airflow #71158&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Airflow&lt;/td&gt;
&lt;td&gt;Metrics vs traces &lt;code&gt;otel_*&lt;/code&gt; config clarity&lt;/td&gt;
&lt;td&gt;Docs clarity; merged after second reviewer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Pattern:&lt;/strong&gt; documentation and operational clarity beat drive-by feature PRs for early upstream contributions. Both changes were easy to review, tied to real production confusion, and did not require deep codebase archaeology.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still open (and what that teaches)
&lt;/h2&gt;

&lt;p&gt;As of late August 2026, four PRs remain in flight:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PR&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Lesson&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/dbt-labs/docs.getdbt.com/pull/9781" rel="noopener noreferrer"&gt;dbt docs #9781&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Awaiting review&lt;/td&gt;
&lt;td&gt;Issue-linked docs fixes still wait on maintainer bandwidth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/meltano/meltano/pull/10253" rel="noopener noreferrer"&gt;Meltano #10253&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Awaiting review&lt;/td&gt;
&lt;td&gt;Tie PRs to maintainer-requested issues (&lt;a href="https://github.com/meltano/meltano/issues/6289" rel="noopener noreferrer"&gt;#6289&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/PrefectHQ/prefect/pull/22533" rel="noopener noreferrer"&gt;Prefect #22533&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Changes requested â†’ addressed&lt;/td&gt;
&lt;td&gt;Automated review catches doc accuracy gaps; respond precisely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/apache/airflow/pull/70171" rel="noopener noreferrer"&gt;Airflow #70171&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Open&lt;/td&gt;
&lt;td&gt;Provider PRs need patience; keep CI green, don't churn&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Airflow &lt;a href="https://github.com/apache/airflow/pull/70185" rel="noopener noreferrer"&gt;#70185&lt;/a&gt;&lt;/strong&gt; was closed when the maintainer wanted a proper OpenLineage facet instead of my initial approach. That was the right outcome â€” don't force the wrong abstraction to keep a PR open.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fewer open PRs at once&lt;/strong&gt; â€” after ~4 in flight, review bandwidth becomes the bottleneck, not ideas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebase early&lt;/strong&gt; â€” Airflow moves fast; waiting weeks breaks CI on unrelated upstream changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portfolio first, then upstream narrative&lt;/strong&gt; â€” shipping quarantine/DLQ in &lt;a href="https://github.com/br413/production-data-pipeline/releases/tag/v0.2.1" rel="noopener noreferrer"&gt;production-data-pipeline v0.2.1&lt;/a&gt; made the &lt;a href="https://dev.to/bobby_ray_581732c715283b2/data-quality-contracts-in-production-pipelines-without-a-separate-platform-team-f3"&gt;data quality contracts article&lt;/a&gt; credible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Close gracefully&lt;/strong&gt; â€” a withdrawn or closed PR with a clear maintainer reason is better than a stale open one&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The weekly rhythm that worked
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day&lt;/th&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mon&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One upstream comment + one small portfolio commit (docs/tests)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OSS PR work, rebase, or CI fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fri&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;README/ADR cross-link, plan update, or writing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Minimum bar:&lt;/strong&gt; three public commit days per week. Consistency beats hero days for both the contribution graph and maintainer trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  How portfolio and OSS reinforce each other
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;production-data-pipeline (ingestion + quarantine)
    â†” data-quality-observability (contracts)
    â†” Dev.to articles (public narrative)
    â†” upstream fixes (Prefect / Airflow / dbt / Meltano ops + docs)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer answers a different reviewer question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Portfolio&lt;/strong&gt; â€” Can you design and ship a production-style stack?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing&lt;/strong&gt; â€” Can you explain trade-offs clearly?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upstream&lt;/strong&gt; â€” Can you improve tools other teams already depend on?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Honest scorecard (90-day closeout)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Upstream merges&lt;/td&gt;
&lt;td&gt;5+&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;3&lt;/strong&gt; â€” in progress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dev.to articles&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;4&lt;/strong&gt; âœ“&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portfolio release&lt;/td&gt;
&lt;td&gt;v0.2.1 + quarantine&lt;/td&gt;
&lt;td&gt;âœ“&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Green contribution weeks&lt;/td&gt;
&lt;td&gt;10+ consecutive&lt;/td&gt;
&lt;td&gt;On track with Mon/Wed/Fri rhythm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The merge count is below target. That is worth stating plainly. The work is still credible because the open PRs are real, issue-linked, and actively maintained â€” not abandoned drive-bys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules I kept (and recommend)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never backdate commits&lt;/strong&gt; â€” the activity graph reflects real work only&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comment before PR&lt;/strong&gt; on upstream issues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer data-platform repos&lt;/strong&gt; (dbt, Airflow, Prefect, Meltano) over unrelated forks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One meaningful merge beats five cosmetic self-PRs&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Profile, portfolio site, and resume must agree&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  If you're starting a similar push
&lt;/h2&gt;

&lt;p&gt;Pick one upstream project you already use in production. Find a docs gap or ops footgun you have actually hit. Comment on the issue. Open a small PR. Ship one portfolio release that gives you standing to write about the same problem space.&lt;/p&gt;

&lt;p&gt;Then repeat on a weekly cadence for ninety days.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related writing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/bobby_ray_581732c715283b2/building-a-production-data-pipeline-with-incremental-loading-and-dbt-2e2c"&gt;Building a Production Data Pipeline with Incremental Loading and dbt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/bobby_ray_581732c715283b2/data-quality-contracts-in-production-pipelines-without-a-separate-platform-team-f3"&gt;Data Quality Contracts in Production Pipelines&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://br413.github.io/" rel="noopener noreferrer"&gt;Portfolio site&lt;/a&gt; Â· &lt;a href="https://github.com/br413" rel="noopener noreferrer"&gt;GitHub profile&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this helped, leave a comment â€” I am interested in how other data engineers approach upstream contributions without turning it into performance theater.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>opensource</category>
      <category>career</category>
      <category>airflow</category>
    </item>
    <item>
      <title>Data Quality Contracts in Production Pipelines (Without a Separate Platform Team)</title>
      <dc:creator>Bobby Ray</dc:creator>
      <pubDate>Fri, 21 Aug 2026 16:59:17 +0000</pubDate>
      <link>https://dev.to/bobby_ray_581732c715283b2/data-quality-contracts-in-production-pipelines-without-a-separate-platform-team-f3</link>
      <guid>https://dev.to/bobby_ray_581732c715283b2/data-quality-contracts-in-production-pipelines-without-a-separate-platform-team-f3</guid>
      <description>&lt;p&gt;Production pipelines fail in two ways: loudly at 3 a.m., or quietly in the CEO's dashboard. &lt;strong&gt;Data quality contracts&lt;/strong&gt; give you a third path â€” catch drift at the boundary, persist evidence, and route bad rows before they poison silver models.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a href="https://br413.github.io/" rel="noopener noreferrer"&gt;br413.github.io&lt;/a&gt; Â· &lt;strong&gt;Quality layer:&lt;/strong&gt; &lt;a href="https://github.com/br413/data-quality-observability" rel="noopener noreferrer"&gt;data-quality-observability&lt;/a&gt; Â· &lt;strong&gt;Ingestion quarantine:&lt;/strong&gt; &lt;a href="https://github.com/br413/production-data-pipeline/releases/tag/v0.2.1" rel="noopener noreferrer"&gt;production-data-pipeline v0.2.1&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem: silent failure modes
&lt;/h2&gt;

&lt;p&gt;Most teams discover data quality problems &lt;strong&gt;downstream&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schema drift lands in bronze; dbt tests fail hours later (or worse, pass on stale assumptions)&lt;/li&gt;
&lt;li&gt;One poison-pill API record aborts an entire ingestion page â€” valid events in the same batch never land&lt;/li&gt;
&lt;li&gt;Operators grep logs instead of querying failure artifacts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is not "more dbt tests." It is &lt;strong&gt;layered gates&lt;/strong&gt; with durable evidence at each boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a contract is (and isn't)
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;data contract&lt;/strong&gt; is a machine-readable agreement about a dataset: column types, null rules, freshness windows, and referential integrity. It is a &lt;strong&gt;gate at the dataset boundary&lt;/strong&gt;, not a replacement for warehouse-level tests.&lt;/p&gt;

&lt;p&gt;Example from my &lt;a href="https://github.com/br413/data-quality-observability" rel="noopener noreferrer"&gt;data-quality-observability&lt;/a&gt; project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Order facts contract for retail analytics&lt;/span&gt;

&lt;span class="na"&gt;freshness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;column&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;updated_at&lt;/span&gt;
  &lt;span class="na"&gt;max_age_hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;48&lt;/span&gt;

&lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;order_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;unique&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;customer_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;order_total&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;number&lt;/span&gt;
    &lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;updated_at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;datetime&lt;/span&gt;
    &lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;referential_integrity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;column&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;
    &lt;span class="na"&gt;references&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customers&lt;/span&gt;
      &lt;span class="na"&gt;column&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the contract against sample data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; src.dqo.cli run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--contract&lt;/span&gt; contracts/orders.yml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; data/samples/orders.csv &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--references&lt;/span&gt; data/samples
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI validates schema, nulls, uniqueness, freshness, and foreign keys â€” then persists run history and routes alerts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What contracts are not:&lt;/strong&gt; they do not replace dbt source freshness or uniqueness tests. They complement them by running &lt;strong&gt;before promote&lt;/strong&gt; with auditable run history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two layers in a real stack
&lt;/h2&gt;

&lt;p&gt;Production quality work spans two boundaries in my portfolio:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Row-level quarantine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;During ingestion&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/br413/production-data-pipeline" rel="noopener noreferrer"&gt;production-data-pipeline&lt;/a&gt; â€” invalid events â†’ &lt;code&gt;bronze.quarantine_events&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dataset contracts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;After landing / before promote&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/br413/data-quality-observability" rel="noopener noreferrer"&gt;data-quality-observability&lt;/a&gt; â€” run history + alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    API[External API] --&amp;gt; Ingest[Ingestion connector]
    Ingest --&amp;gt;|valid record| Bronze[bronze.raw_events]
    Ingest --&amp;gt;|validation failure| Quarantine[bronze.quarantine_events]
    Bronze --&amp;gt; Silver[dbt stg_events]
    Quarantine -.-&amp;gt;|excluded by NOT EXISTS| Silver
    Silver --&amp;gt; Gold[fct_daily_event_metrics]
    Bronze --&amp;gt; Contract[YAML contract checks]
    Contract --&amp;gt; History[Run history store]
    Contract --&amp;gt; Alerts[Alert router]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Row-level quarantine at ingestion
&lt;/h3&gt;

&lt;p&gt;Before v0.2.1, my pipeline used &lt;strong&gt;fail-fast batch validation&lt;/strong&gt;: one bad record aborted the entire page. &lt;a href="https://github.com/br413/production-data-pipeline/blob/main/docs/adr/0004-failed-record-quarantine.md" rel="noopener noreferrer"&gt;ADR 0004&lt;/a&gt; introduces per-record routing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pass&lt;/strong&gt; â†’ land in &lt;code&gt;bronze.raw_events&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail (recoverable)&lt;/strong&gt; â†’ write to &lt;code&gt;bronze.quarantine_events&lt;/code&gt; with rule name, message, and raw payload&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checkpoint&lt;/strong&gt; â†’ quarantined &lt;code&gt;event_id&lt;/code&gt; values are marked processed so poison pills do not block retries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enable quarantine during ingestion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; src.pipeline.ingestion &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; sample &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--storage&lt;/span&gt; postgres &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pipeline-name&lt;/span&gt; sample-ingestion &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-quarantine&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--alert-on-quarantine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The quarantine table schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;bronze&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quarantine_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;occurred_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;failed_rule&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;failure_message&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pipeline_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;quarantined_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipeline_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Silver models exclude quarantined IDs explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;occurred_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'value'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;event_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ingested_at&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bronze'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'raw_events'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;'value'&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bronze'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'quarantine_events'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;
    &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Operator triage&lt;/strong&gt; becomes a SQL query instead of a log grep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failed_rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quarantined_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bronze&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quarantine_events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;pipeline_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sample-ingestion'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;quarantined_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ingestion summary now includes &lt;code&gt;records_quarantined&lt;/code&gt; alongside fetched, inserted, and skipped counts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dataset contracts after landing
&lt;/h3&gt;

&lt;p&gt;The quality observability layer runs &lt;strong&gt;after&lt;/strong&gt; data lands but &lt;strong&gt;before&lt;/strong&gt; you trust it for analytics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Data contract (YAML)
    â†“
Quality check suite (schema Â· null Â· unique Â· freshness Â· RI)
    â†“
Run summary â†’ history store (SQLite / PostgreSQL)
    â†“
Alert router (console Â· JSONL file Â· webhook)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schedule it with the included Airflow DAG (&lt;code&gt;dqo_contract_checks&lt;/code&gt;) or run ad hoc from CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alert routing that on-call will answer
&lt;/h2&gt;

&lt;p&gt;Alerts must be &lt;strong&gt;actionable&lt;/strong&gt;. Both projects route to console, file, and webhook channels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zero_record_ingestion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;production-data-pipeline&lt;/td&gt;
&lt;td&gt;Ingestion succeeded but landed zero new records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ingestion_quarantine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;production-data-pipeline&lt;/td&gt;
&lt;td&gt;One or more records routed to quarantine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contract failure&lt;/td&gt;
&lt;td&gt;data-quality-observability&lt;/td&gt;
&lt;td&gt;Schema, freshness, or RI check failed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Webhook payloads include structured summaries â€” pipeline name, quarantine count, failed rule â€” so on-call can triage without opening the repo.&lt;/p&gt;

&lt;p&gt;Tie alerts to &lt;strong&gt;operations runbooks&lt;/strong&gt;: query the quarantine table, inspect the failed payload, fix the source or rule, then replay or manually promote the row.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI as contract enforcement
&lt;/h2&gt;

&lt;p&gt;Green CI means the &lt;strong&gt;contract suite ran&lt;/strong&gt;, not that production data is clean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;production-data-pipeline&lt;/strong&gt; â€” pytest covers quarantine routing, webhook event types, and dbt integration (including quarantine exclusion in &lt;code&gt;stg_events&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;data-quality-observability&lt;/strong&gt; â€” pytest covers check logic; integration tests mock webhook delivery success and failure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub Actions runs both suites on every push. Contract YAML changes trigger the same gates as code changes â€” drift in the contract file is caught before merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical adoption path
&lt;/h2&gt;

&lt;p&gt;You do not need a dedicated data quality platform team to start:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pick one high-value dataset&lt;/strong&gt; â€” orders, events, or your most-used fact table&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add freshness + required-field checks&lt;/strong&gt; before adding referential integrity complexity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable quarantine/DLQ&lt;/strong&gt; when API sources are messy or batch fail-fast is blocking valid records&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wire alerts&lt;/strong&gt; to your existing webhook or Slack path â€” reuse the same router for ingestion and contract failures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document recovery&lt;/strong&gt; in an operations runbook so 2 AM triage does not depend on tribal knowledge&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How this connects to article #1
&lt;/h2&gt;

&lt;p&gt;My first Dev.to article covered &lt;a href="https://dev.to/bobby_ray_581732c715283b2/building-a-production-data-pipeline-with-incremental-loading-and-dbt-2e2c"&gt;incremental ingestion, checkpoints, and medallion layering with dbt&lt;/a&gt;. That pipeline intentionally ended with "at scale I would add a dead-letter queue and a separate quality layer."&lt;/p&gt;

&lt;p&gt;This article is the follow-through: &lt;strong&gt;quarantine at ingestion&lt;/strong&gt; (v0.2.1) and &lt;strong&gt;contracts at the quality boundary&lt;/strong&gt; (data-quality-observability), wired together as a production-style stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Quality contracts:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/br413/data-quality-observability.git
&lt;span class="nb"&gt;cd &lt;/span&gt;data-quality-observability
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="c"&gt;# Windows: .\.venv\Scripts\Activate.ps1&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
pytest
python &lt;span class="nt"&gt;-m&lt;/span&gt; src.dqo.cli run &lt;span class="nt"&gt;--contract&lt;/span&gt; contracts/orders.yml &lt;span class="nt"&gt;--data&lt;/span&gt; data/samples/orders.csv &lt;span class="nt"&gt;--references&lt;/span&gt; data/samples
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ingestion quarantine:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/br413/production-data-pipeline.git
&lt;span class="nb"&gt;cd &lt;/span&gt;production-data-pipeline
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; src.pipeline.ingestion &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; sample &lt;span class="nt"&gt;--storage&lt;/span&gt; postgres &lt;span class="nt"&gt;--pipeline-name&lt;/span&gt; sample-ingestion &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-quarantine&lt;/span&gt; &lt;span class="nt"&gt;--alert-on-quarantine&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; src.pipeline.run_dbt &lt;span class="nt"&gt;--target&lt;/span&gt; dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both repos include ADRs, operations runbooks, and CI with PostgreSQL service containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Data quality is a &lt;strong&gt;stack of boundaries&lt;/strong&gt;, not a single tool. Row-level quarantine keeps poison pills from blocking valid ingestion. Dataset contracts catch drift before promote. Persisted failure artifacts and webhook alerts turn silent dashboard bugs into triageable incidents.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related projects&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/br413/production-data-pipeline/releases/tag/v0.2.1" rel="noopener noreferrer"&gt;production-data-pipeline v0.2.1&lt;/a&gt; â€” quarantine/DLQ at ingestion&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/br413/data-quality-observability" rel="noopener noreferrer"&gt;data-quality-observability&lt;/a&gt; â€” YAML contracts and run history&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/br413/cloud-lakehouse-blueprint" rel="noopener noreferrer"&gt;cloud-lakehouse-blueprint&lt;/a&gt; â€” platform governance and lineage&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://br413.github.io/" rel="noopener noreferrer"&gt;Portfolio site&lt;/a&gt; Â· &lt;a href="https://github.com/br413" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this helped, &lt;strong&gt;star the repos&lt;/strong&gt; or leave a comment â€” I am interested in how other teams layer contracts and quarantine in production.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>python</category>
      <category>dbt</category>
      <category>dataquality</category>
    </item>
    <item>
      <title>Building a Production Data Pipeline with Incremental Loading and dbt</title>
      <dc:creator>Bobby Ray</dc:creator>
      <pubDate>Sun, 19 Jul 2026 19:15:54 +0000</pubDate>
      <link>https://dev.to/bobby_ray_581732c715283b2/building-a-production-data-pipeline-with-incremental-loading-and-dbt-2e2c</link>
      <guid>https://dev.to/bobby_ray_581732c715283b2/building-a-production-data-pipeline-with-incremental-loading-and-dbt-2e2c</guid>
      <description>&lt;p&gt;Operational analytics breaks when pipelines silently drop records, re-process duplicates, or push schema drift into dashboards. This article walks through a &lt;strong&gt;production-style data pipeline&lt;/strong&gt; pattern: incremental API ingestion, explicit checkpoints, medallion-style layering, and orchestration with Apache Airflow and dbt.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a href="https://br413.github.io/" rel="noopener noreferrer"&gt;br413.github.io&lt;/a&gt; Â· &lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/br413/production-data-pipeline" rel="noopener noreferrer"&gt;production-data-pipeline&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Most demo pipelines assume APIs are always available, schemas never change, and re-runs are harmless. Production systems need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Incremental loading&lt;/strong&gt; â€” fetch only new data since the last successful run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency&lt;/strong&gt; â€” duplicate deliveries must not inflate metrics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separable layers&lt;/strong&gt; â€” raw landing, validated staging, and curated aggregates tested independently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational visibility&lt;/strong&gt; â€” know when a run ingested zero records or failed mid-page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your pipeline cannot recover at 2 AM without tribal knowledge, it is not production-ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;External API
    â†“
Ingestion connector (incremental, idempotent)
    â†“
Raw / bronze layer (PostgreSQL landing)
    â†“
Validated / silver layer (dbt stg_events)
    â†“
Curated / gold layer (fct_daily_event_metrics)
    â†“
Downstream consumers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each boundary has a clear responsibility. Ingestion handles pagination and checkpoint advancement. Bronze stores append-only raw events. dbt owns transformation logic and data tests. Airflow schedules and retries the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key design decisions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Checkpoint + processed-ID tracking&lt;/td&gt;
&lt;td&gt;Timestamp watermarks fail when APIs return out-of-order events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL bronze landing&lt;/td&gt;
&lt;td&gt;DB constraints enforce idempotency; matches warehouse patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quality gates before bronze&lt;/td&gt;
&lt;td&gt;Block bad records at ingestion, not after they pollute raw history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt for silver/gold&lt;/td&gt;
&lt;td&gt;Declarative transforms with built-in tests and clearer ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Incremental ingestion with checkpoints
&lt;/h2&gt;

&lt;p&gt;The ingestion connector tracks its read position in a &lt;strong&gt;checkpoint store&lt;/strong&gt;. Two options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;File-based checkpoints&lt;/strong&gt; â€” lightweight for local development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL metadata tables&lt;/strong&gt; â€” durable for shared environments
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Local run with file checkpoint&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; src.pipeline.ingestion &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; sample &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--checkpoint&lt;/span&gt; .checkpoints/sample.json

&lt;span class="c"&gt;# Production-style run with PostgreSQL&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; src.pipeline.ingestion &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; sample &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--storage&lt;/span&gt; postgres &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pipeline-name&lt;/span&gt; sample-ingestion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key behaviors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Checkpoints advance only after successful page processing&lt;/strong&gt; â€” a timeout mid-page does not skip data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stable event IDs suppress duplicates&lt;/strong&gt; â€” re-delivered events are ignored via a processed-ID set&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality gates run before landing&lt;/strong&gt; â€” schema validation, required fields, and freshness checks block bad records from bronze&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bronze, silver, and gold with dbt
&lt;/h2&gt;

&lt;p&gt;After landing in &lt;code&gt;bronze.raw_events&lt;/code&gt;, dbt builds two layers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Silver&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stg_events&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Validated staging with typed columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gold&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fct_daily_event_metrics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Daily aggregates for analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; src.pipeline.run_dbt &lt;span class="nt"&gt;--target&lt;/span&gt; dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;dbt tests enforce uniqueness, not-null constraints, and source freshness. When a test fails, the pipeline stops â€” downstream consumers never see broken data.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;medallion architecture&lt;/strong&gt; in miniature: raw â†’ validated â†’ curated, each layer independently testable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Orchestration with Airflow
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;production_sample_ingestion&lt;/code&gt; DAG runs two tasks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ingest_sample_events&lt;/code&gt;&lt;/strong&gt; â€” Python ingestion into bronze&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;run_dbt_models&lt;/code&gt;&lt;/strong&gt; â€” dbt run + test for silver and gold&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Default retry policy: 2 retries with a 5-minute delay. For local testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;airflow dags &lt;span class="nb"&gt;test &lt;/span&gt;production_sample_ingestion 2026-07-14
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Failure modes and mitigations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Mitigation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate delivery&lt;/td&gt;
&lt;td&gt;Processed-ID set suppresses re-ingestion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partial page read&lt;/td&gt;
&lt;td&gt;Checkpoint advances only after full page success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API timeout&lt;/td&gt;
&lt;td&gt;Airflow retry + documented manual rerun&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema drift&lt;/td&gt;
&lt;td&gt;Python quality gate blocks landing; dbt tests catch transform issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt test failure&lt;/td&gt;
&lt;td&gt;DAG stops before downstream use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero-record success&lt;/td&gt;
&lt;td&gt;Webhook alerts for empty ingestion runs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Document these in an operations runbook. Pipelines that cannot be recovered at 2 AM are not production-ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability: metrics and alerts
&lt;/h2&gt;

&lt;p&gt;Recent additions to the project include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structured ingestion summary metrics&lt;/strong&gt; â€” records fetched, inserted, skipped per run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook alerts&lt;/strong&gt; for zero-record runs and Airflow task failures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local validation script&lt;/strong&gt; (&lt;code&gt;scripts/check.ps1&lt;/code&gt;) for pre-commit confidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Observability is not optional. A pipeline that runs successfully but ingests zero records is often worse than one that fails loudly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently at scale
&lt;/h2&gt;

&lt;p&gt;This portfolio project targets a single API source and PostgreSQL landing. In a larger platform I would add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dead-letter queue&lt;/strong&gt; for records that fail quality gates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema registry&lt;/strong&gt; or contract versioning for API drift&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lineage tracking&lt;/strong&gt; from source through gold (see my &lt;a href="https://github.com/br413/cloud-lakehouse-blueprint" rel="noopener noreferrer"&gt;cloud-lakehouse-blueprint&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate quality observability layer&lt;/strong&gt; (see &lt;a href="https://github.com/br413/data-quality-observability" rel="noopener noreferrer"&gt;data-quality-observability&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/br413/production-data-pipeline.git
&lt;span class="nb"&gt;cd &lt;/span&gt;production-data-pipeline
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate  &lt;span class="c"&gt;# Windows: .\.venv\Scripts\Activate.ps1&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
pytest
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; src.pipeline.ingestion &lt;span class="nt"&gt;--source&lt;/span&gt; sample &lt;span class="nt"&gt;--storage&lt;/span&gt; postgres &lt;span class="nt"&gt;--pipeline-name&lt;/span&gt; sample-ingestion
python &lt;span class="nt"&gt;-m&lt;/span&gt; src.pipeline.run_dbt &lt;span class="nt"&gt;--target&lt;/span&gt; dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repo includes architecture docs, ADRs, CI with PostgreSQL service containers, and a &lt;a href="https://github.com/br413/production-data-pipeline/releases/tag/v0.1.0" rel="noopener noreferrer"&gt;v0.1.0 release&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Production data pipelines are defined by how they handle failure, not how they handle the happy path. Incremental checkpoints, idempotent loads, medallion layering, and testable transformation boundaries give you a foundation that scales from portfolio projects to real cloud data platforms.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related projects&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/br413/data-quality-observability" rel="noopener noreferrer"&gt;data-quality-observability&lt;/a&gt; â€” contract-driven quality checks&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/br413/cloud-lakehouse-blueprint" rel="noopener noreferrer"&gt;cloud-lakehouse-blueprint&lt;/a&gt; â€” medallion lakehouse with Terraform&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://br413.github.io/" rel="noopener noreferrer"&gt;Portfolio site&lt;/a&gt; Â· &lt;a href="https://github.com/br413" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; â€” more data engineering work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this helped, &lt;strong&gt;star the repo&lt;/strong&gt; on GitHub or leave a comment â€” I am interested in feedback from other data engineers and platform architects building similar systems.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>python</category>
      <category>dbt</category>
      <category>airflow</category>
    </item>
  </channel>
</rss>
