<?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>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;Full source code: &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;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 cursor 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;
  
  
  Incremental ingestion with checkpoints
&lt;/h2&gt;

&lt;p&gt;The ingestion connector tracks cursor 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 design decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cursor advances 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;/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://github.com/br413" rel="noopener noreferrer"&gt;GitHub portfolio&lt;/a&gt; — more data engineering work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you found this useful, star the repo or leave a comment — I am always interested in feedback from other data engineers and architects.&lt;/p&gt;

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