<?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: elkanah karera</title>
    <description>The latest articles on DEV Community by elkanah karera (@elikanah890).</description>
    <link>https://dev.to/elikanah890</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%2F4238076%2F94e16cd7-5d13-4334-86fd-0ef25bb60b1c.jpg</url>
      <title>DEV Community: elkanah karera</title>
      <link>https://dev.to/elikanah890</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/elikanah890"/>
    <language>en</language>
    <item>
      <title>Building a Medallion Data Warehouse with Airflow, dbt, and PostgreSQL — and the 3 Bugs That Taught Me the Most</title>
      <dc:creator>elkanah karera</dc:creator>
      <pubDate>Wed, 11 Nov 2026 01:01:48 +0000</pubDate>
      <link>https://dev.to/elikanah890/building-a-medallion-data-warehouse-with-airflow-dbt-and-postgresql-and-the-3-bugs-that-taught-371e</link>
      <guid>https://dev.to/elikanah890/building-a-medallion-data-warehouse-with-airflow-dbt-and-postgresql-and-the-3-bugs-that-taught-371e</guid>
      <description>&lt;p&gt;I had nine Olist e-commerce CSVs — 1.55 million rows — and no pipeline. I wanted a production-shaped batch pipeline, not another tutorial notebook.&lt;/p&gt;

&lt;p&gt;This is what I built, and the three bugs that taught me more than the code did.&lt;/p&gt;

&lt;p&gt;Medallion pattern on PostgreSQL:&lt;/p&gt;

&lt;p&gt;9 CSVs → Bronze (raw + SHA-256 audit) → Silver (typed + flagged) → Gold (star schema)&lt;/p&gt;

&lt;p&gt;Bronze keeps every column as TEXT — no casting. Each file is SHA-256 hashed, and if the hash is already loaded, it skips. Malformed rows go to an audit table, never dropped silently.&lt;/p&gt;

&lt;p&gt;Silver applies real types, PK/FK constraints, and dedup. Key decision: retain and flag anomalies instead of deleting them — 1,382 delivery anomalies, 1,076 payment mismatches, 610 uncategorized products. Dropping rows silently changes business totals.&lt;/p&gt;

&lt;p&gt;Gold builds 6 dimensions, 4 facts, 9 views. fact_orders pre-aggregates items, payments, and reviews independently before joining to avoid fan-out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The orchestration
&lt;/h2&gt;

&lt;p&gt;Five Airflow tasks, two blocking gates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bronze_load → 0.86s (hash-skip)&lt;/li&gt;
&lt;li&gt;bronze_quality_gate → 0.34s&lt;/li&gt;
&lt;li&gt;silver_rebuild → 45.01s&lt;/li&gt;
&lt;li&gt;silver_quality_gate → 3.53s&lt;/li&gt;
&lt;li&gt;gold_build → 7.70s&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Green run: 57 seconds. The 0.86s on Bronze isn't a bug — it's SHA-256 idempotency working.&lt;/p&gt;

&lt;p&gt;The DAG does not pass DataFrames between tasks. XComs are for metadata, not datasets.&lt;/p&gt;

&lt;p&gt;dbt runs in parallel: 33 models, 162 tests, isolated dbt_dev_* schemas with a fail-closed macro.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: Airflow couldn't reach the database
&lt;/h2&gt;

&lt;p&gt;PostgreSQL was running, but the container got "Connection refused."&lt;/p&gt;

&lt;p&gt;The fix: PostgreSQL was bound only to loopback. Changed listen_addresses in postgresql.conf to include 172.17.0.1 (Docker bridge) and added a pg_hba.conf rule for 172.17.0.0/16.&lt;/p&gt;

&lt;p&gt;Container-to-host networking is a host config problem, not a container problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: Relative paths resolved wrong
&lt;/h2&gt;

&lt;p&gt;The DAG failed with FileNotFoundError on data/raw/olist_customers_dataset.csv.&lt;/p&gt;

&lt;p&gt;.env said RAW_DIR=./data/raw — correct on the host, wrong inside the container where CWD is /opt/airflow.&lt;/p&gt;

&lt;p&gt;The fix: override DATA_DIR, RAW_DIR, PROCESSED_DIR, WAREHOUSE_DIR in the container environment, not in .env. Changing .env would break host-side runs.&lt;/p&gt;

&lt;p&gt;Container overrides belong in the container. Not shared config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: pandas version conflict
&lt;/h2&gt;

&lt;p&gt;Airflow 2.9.3's constraints file pins pandas==2.1.4. My Dockerfile asked for pandas&amp;gt;=2.2.&lt;/p&gt;

&lt;p&gt;The fix: one line — pandas==2.1.4.&lt;/p&gt;

&lt;p&gt;The --constraint file is not a suggestion. It's the tested version set.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I deliberately did NOT build
&lt;/h2&gt;

&lt;p&gt;Not streaming — the source is static CSVs, Kafka would be theatre.&lt;/p&gt;

&lt;p&gt;Not incremental — full rebuilds (TRUNCATE + DROP) every run. Fine for 1.55M rows, documented as an ADR with the migration path.&lt;/p&gt;

&lt;p&gt;Not production-scale — 1.55M rows is small. I say "production-shaped" because one exaggeration makes reviewers distrust everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Raw rows: 1,550,922&lt;/li&gt;
&lt;li&gt;Gold objects: 6 dims, 4 facts, 9 views&lt;/li&gt;
&lt;li&gt;dbt models / tests: 33 / 162&lt;/li&gt;
&lt;li&gt;Green run: 57.4 seconds&lt;/li&gt;
&lt;li&gt;Rejected rows: 0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zero rejects means malformed rows went to audit tables — not that the data is pristine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real lesson
&lt;/h2&gt;

&lt;p&gt;The pipeline was the easy part. The hard part is the operating discipline: idempotent ingestion, quality gates that block, documenting trade-offs, and admitting what you didn't build.&lt;/p&gt;

&lt;p&gt;A data engineering portfolio isn't a list of technologies. It's evidence you can ship something and run it.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/Elikanah890/Enterprise-E-Commerce-Data-Engineering-Platform" rel="noopener noreferrer"&gt;https://github.com/Elikanah890/Enterprise-E-Commerce-Data-Engineering-Platform&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Stack: PostgreSQL 16 · Airflow 2.9.3 · dbt 1.9.4 · Python 3.11 · Docker Compose&lt;/p&gt;

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