<?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: Pradeep Kalluri</title>
    <description>The latest articles on DEV Community by Pradeep Kalluri (@pradeep_kaalluri).</description>
    <link>https://dev.to/pradeep_kaalluri</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3608785%2F5c9181fc-36e7-4fc5-9655-c4bfe020df17.jpg</url>
      <title>DEV Community: Pradeep Kalluri</title>
      <link>https://dev.to/pradeep_kaalluri</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pradeep_kaalluri"/>
    <language>en</language>
    <item>
      <title>Rewriting My Apache Airflow PR: When Your First Solution Isn't the Right One</title>
      <dc:creator>Pradeep Kalluri</dc:creator>
      <pubDate>Mon, 05 Jan 2026 09:46:50 +0000</pubDate>
      <link>https://dev.to/pradeep_kaalluri/rewriting-my-apache-airflow-pr-when-your-first-solution-isnt-the-right-one-1mp7</link>
      <guid>https://dev.to/pradeep_kaalluri/rewriting-my-apache-airflow-pr-when-your-first-solution-isnt-the-right-one-1mp7</guid>
      <description>&lt;p&gt;Lessons learned from contributing to Apache Airflow after getting my first PR merged - complete rewrite, 7 CI failures, and persistence&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmtk18bw5jydyrw0pmn5w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmtk18bw5jydyrw0pmn5w.png" alt=" " width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After getting my first Apache Airflow PR merged (#58587), I felt pretty confident about the contribution process. So when I found another bug, I jumped right in with what seemed like a perfect solution.&lt;/p&gt;

&lt;p&gt;Two weeks and a complete rewrite later, my second PR (#59938) is now &lt;strong&gt;merged into Apache Airflow&lt;/strong&gt;. Here's the real story—the good, the messy, and what changed.&lt;/p&gt;




&lt;h2&gt;
  
  
  🐛 Finding the Bug
&lt;/h2&gt;

&lt;p&gt;It started with a production issue I encountered. Our Airflow scheduler kept crashing with a cryptic error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;InvalidStatsNameException: The stat name (pool.running_slots.data engineering pool 😎) 
has to be composed of ASCII alphabets, numbers, or the underscore, dot, or dash characters.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Someone had created a pool with spaces and an emoji. Airflow accepted it, but when trying to report metrics, everything broke.&lt;/p&gt;

&lt;p&gt;Having just gotten my first PR merged, I thought: "I know how this works now. I can fix this quickly."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spoiler: It wasn't quick.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  💻 My "Perfect" Solution: Validation
&lt;/h2&gt;

&lt;p&gt;My approach seemed obvious:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add validation when creating pools&lt;/li&gt;
&lt;li&gt;Only allow ASCII letters, numbers, underscores, dots, and dashes&lt;/li&gt;
&lt;li&gt;Reject invalid pool names with a clear error
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_pool_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^[a-zA-Z0-9_.-]+$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pool name &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; is invalid. Pool names must only contain &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ASCII alphabets, numbers, underscores, dots, and dashes.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrote tests, updated the news fragment, and submitted the PR with confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem solved!&lt;/strong&gt; Or so I thought.&lt;/p&gt;




&lt;h2&gt;
  
  
  💥 The Feedback That Humbled Me
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/potiuk" rel="noopener noreferrer"&gt;@potiuk&lt;/a&gt; (Apache Airflow PMC member) reviewed my PR:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I do not think it's a good idea to raise issue at pool creation time. This will mean that when you create an invalid pool, things will start crashing soon after. That's quite wrong behaviour."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He suggested a completely different approach: &lt;strong&gt;normalize the pool names for stats reporting instead of preventing them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My heart sank. I'd spent hours on this validation approach, written tests, updated docs. But he was right:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problems With My Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Users with existing "invalid" pools would be stuck&lt;/li&gt;
&lt;li&gt;❌ Migration would be complex and painful&lt;/li&gt;
&lt;li&gt;❌ It would break backward compatibility&lt;/li&gt;
&lt;li&gt;❌ Pools would be created but then crash the scheduler&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Better Approach:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Keep existing pools working&lt;/li&gt;
&lt;li&gt;✅ Normalize names only for stats reporting&lt;/li&gt;
&lt;li&gt;✅ Warn users, but don't break their systems&lt;/li&gt;
&lt;li&gt;✅ Graceful degradation instead of failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lesson 1: "Working" code isn't the same as "right" code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first PR was accepted with minor tweaks. This time, I needed to completely rethink the solution.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔄 The Rewrite: Normalization
&lt;/h2&gt;

&lt;p&gt;I threw away my validation code and started fresh:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_pool_name_for_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Normalize pool name for stats reporting by replacing invalid characters.

    Stats names must only contain ASCII alphabets, numbers, underscores, 
    dots, and dashes. Invalid characters are replaced with underscores.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Check if normalization is needed
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^[a-zA-Z0-9_.-]+$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;

    &lt;span class="c1"&gt;# Replace invalid characters with underscores
&lt;/span&gt;    &lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[^a-zA-Z0-9_.-]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Log warning
&lt;/span&gt;    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pool name &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; contains invalid characters for stats reporting. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reporting stats with normalized name &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Consider renaming the pool to avoid this warning.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;normalized&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of preventing "bad" pool names, we:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Accept any pool name (backward compatible)&lt;/li&gt;
&lt;li&gt;Normalize it when reporting metrics (fixes the crash)&lt;/li&gt;
&lt;li&gt;Log a warning (educates users)&lt;/li&gt;
&lt;li&gt;Suggest renaming (guides to best practices)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;This was objectively better.&lt;/strong&gt; And I would never have thought of it without the feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson 2: Maintainers see the bigger picture. Listen to them.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  😤 The Static Check Marathon
&lt;/h2&gt;

&lt;p&gt;I pushed my rewritten code. CI failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ Missing blank lines
❌ Import order wrong  
❌ LoggingMixin usage incorrect
❌ Missing 're' module import
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I fixed them. Pushed again. &lt;strong&gt;CI failed again&lt;/strong&gt; with different formatting issues.&lt;/p&gt;

&lt;p&gt;This happened &lt;strong&gt;SEVEN TIMES.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;CI would auto-format and show what it wanted&lt;/li&gt;
&lt;li&gt;I'd try to apply the fixes manually (Windows, no local pre-commit)&lt;/li&gt;
&lt;li&gt;I'd push&lt;/li&gt;
&lt;li&gt;New formatting errors would appear&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By attempt #5, I was frustrated. By attempt #7, I was questioning my career choices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson 3: Set up your local environment properly BEFORE you start coding.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ The Breakthrough
&lt;/h2&gt;

&lt;p&gt;On attempt #8, I finally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Used PowerShell to apply exact formatting from CI diffs&lt;/li&gt;
&lt;li&gt;Added proper blank lines (2 after logger, 2 after functions)&lt;/li&gt;
&lt;li&gt;Fixed import order alphabetically&lt;/li&gt;
&lt;li&gt;Replaced LoggingMixin with module-level logger
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_pool_name_for_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Function code...
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;normalized&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Class code...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;All checks passed! 🎉&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/potiuk" rel="noopener noreferrer"&gt;@potiuk&lt;/a&gt; approved with "Two nits" (which I quickly fixed). Minutes later, the PR was &lt;strong&gt;merged into Apache Airflow's main branch&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson 4: Persistence beats perfection. Keep going.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 First PR vs Second PR: The Differences
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;My First PR (#58587):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⏱️ Time: 3 days&lt;/li&gt;
&lt;li&gt;🔄 Major rewrites: 0&lt;/li&gt;
&lt;li&gt;❌ CI failures: 2&lt;/li&gt;
&lt;li&gt;📝 Commits: 4&lt;/li&gt;
&lt;li&gt;🎓 Learned: The contribution process&lt;/li&gt;
&lt;li&gt;✅ Status: Merged&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;My Second PR (#59938):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⏱️ Time: 2 weeks&lt;/li&gt;
&lt;li&gt;🔄 Major rewrites: 1 (complete approach change)&lt;/li&gt;
&lt;li&gt;❌ CI failures: 7&lt;/li&gt;
&lt;li&gt;📝 Commits: 16&lt;/li&gt;
&lt;li&gt;🎓 Learned: How to handle feedback, rewrites, and persistence&lt;/li&gt;
&lt;li&gt;✅ Status: &lt;strong&gt;Merged into Apache Airflow&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The second one taught me WAY more.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 What I Learned (That My First PR Didn't Teach Me)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Technical Lessons:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Think About Backward Compatibility&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first solution would have broken existing users. Always ask: "What happens to people already using this?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Graceful Degradation &amp;gt; Hard Failures&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Warn users and normalize data instead of crashing. Systems should be resilient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Pre-commit Hooks Are Non-Negotiable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don't use CI as your formatter. Set up &lt;code&gt;pre-commit&lt;/code&gt; locally FIRST.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Read Diffs Carefully&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;CI was telling me exactly what it wanted. I just needed to pay attention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Soft Skills Lessons:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Be Ready to Throw Away Your Work&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I spent hours on validation code. All of it went in the trash. That's okay. It's part of learning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Feedback Isn't Personal&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Potiuk wasn't criticizing me. He was helping me build something better. There's a huge difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Persistence Matters More Than Talent&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;7 CI failures felt embarrassing. But I kept going, and eventually it worked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Document Your Thinking&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In my PR description, I explained WHY I chose normalization after feedback. This helped reviewers understand my thought process.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Advice for Your Second (or Third, or Tenth) PR
&lt;/h2&gt;

&lt;h3&gt;
  
  
  When You Get Critical Feedback:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Don't:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Defend your solution immediately&lt;/li&gt;
&lt;li&gt;❌ Make minimal changes hoping they'll accept it&lt;/li&gt;
&lt;li&gt;❌ Take it personally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Do:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Read the feedback carefully (twice!)&lt;/li&gt;
&lt;li&gt;✅ Ask questions if you don't understand&lt;/li&gt;
&lt;li&gt;✅ Be willing to start over if needed&lt;/li&gt;
&lt;li&gt;✅ Thank reviewers for their time&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When CI Keeps Failing:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Don't:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Push 10 commits trying to guess the fix&lt;/li&gt;
&lt;li&gt;❌ Ignore error messages&lt;/li&gt;
&lt;li&gt;❌ Give up after 3 failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Do:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Set up local pre-commit hooks&lt;/li&gt;
&lt;li&gt;✅ Read the CI diff output carefully&lt;/li&gt;
&lt;li&gt;✅ Apply fixes locally and test before pushing&lt;/li&gt;
&lt;li&gt;✅ Ask for help if you're stuck after 3-4 attempts&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When You Need to Rewrite:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Don't:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Try to salvage the old approach&lt;/li&gt;
&lt;li&gt;❌ Rush the rewrite&lt;/li&gt;
&lt;li&gt;❌ Skip tests because you're frustrated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Do:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Start with a clean slate&lt;/li&gt;
&lt;li&gt;✅ Think through the new approach carefully&lt;/li&gt;
&lt;li&gt;✅ Write better tests based on what you learned&lt;/li&gt;
&lt;li&gt;✅ Update documentation to match&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚀 Why You Should Keep Contributing
&lt;/h2&gt;

&lt;p&gt;My first PR was smooth sailing. My second was rough waters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's exactly how learning works.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each contribution teaches you something new:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First PR:&lt;/strong&gt; The basics (fork, commit, PR, review)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Second PR:&lt;/strong&gt; Handling feedback and major rewrites
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third PR:&lt;/strong&gt; You'll discover this next!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For your career:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-world experience with design decisions&lt;/li&gt;
&lt;li&gt;Proof you can handle feedback and pivot&lt;/li&gt;
&lt;li&gt;Stories to tell in interviews ("I once had to completely rewrite my approach...")&lt;/li&gt;
&lt;li&gt;Connections with senior engineers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For your skills:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understanding trade-offs (validation vs normalization)&lt;/li&gt;
&lt;li&gt;Production-quality code standards&lt;/li&gt;
&lt;li&gt;Communication under pressure&lt;/li&gt;
&lt;li&gt;Resilience and persistence&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  💡 Your Turn
&lt;/h2&gt;

&lt;p&gt;If you've contributed once and it went well, &lt;strong&gt;do it again.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The second one will probably be harder. You might get asked to rewrite. CI might fail repeatedly. Reviewers might question your approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's when the real learning happens.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My second PR took 4x longer than my first. It also taught me 10x more.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What will your next contribution teach you?&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📎 Resources
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;My Merged PRs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First PR (Merged): &lt;a href="https://github.com/apache/airflow/pull/58587" rel="noopener noreferrer"&gt;apache/airflow#58587&lt;/a&gt; ✅&lt;/li&gt;
&lt;li&gt;Second PR (Merged): &lt;a href="https://github.com/apache/airflow/pull/59938" rel="noopener noreferrer"&gt;apache/airflow#59938&lt;/a&gt; ✅
&lt;/li&gt;
&lt;li&gt;Issue: &lt;a href="https://github.com/apache/airflow/issues/59935" rel="noopener noreferrer"&gt;apache/airflow#59935&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Helpful Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/apache/airflow/blob/main/CONTRIBUTING.rst" rel="noopener noreferrer"&gt;Airflow Contributing Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pre-commit.com/" rel="noopener noreferrer"&gt;How to set up pre-commit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Originally published on Medium:&lt;/strong&gt; &lt;a href="https://medium.com/@kalluripradeep99/rewriting-my-apache-airflow-pr-when-your-first-solution-isnt-the-right-one-8c4243ca9daf" rel="noopener noreferrer"&gt;https://medium.com/@kalluripradeep99/rewriting-my-apache-airflow-pr-when-your-first-solution-isnt-the-right-one-8c4243ca9daf&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dataengineering</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The Time Our Pipeline Processed the Same Day’s Data 47 Times</title>
      <dc:creator>Pradeep Kalluri</dc:creator>
      <pubDate>Wed, 17 Dec 2025 15:29:11 +0000</pubDate>
      <link>https://dev.to/pradeep_kaalluri/the-time-our-pipeline-processed-the-same-days-data-47-times-5eoh</link>
      <guid>https://dev.to/pradeep_kaalluri/the-time-our-pipeline-processed-the-same-days-data-47-times-5eoh</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv9et8nxsikho8hiuu17d.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv9et8nxsikho8hiuu17d.webp" alt=" " width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I noticed something odd in our Airflow logs on Monday morning. Our daily data pipeline had run multiple times over the weekend instead of once per day.&lt;/p&gt;

&lt;p&gt;Not just a few extra runs. Forty-seven executions. All processing the same day's data: December 3rd.&lt;/p&gt;

&lt;p&gt;Each run showed as successful. No errors. No alerts. Just the same date being processed over and over.&lt;/p&gt;

&lt;p&gt;Here's what happened and what I learned about retry logic that I wish I'd known sooner.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I Found It
&lt;/h2&gt;

&lt;p&gt;Monday morning, I was reviewing our weekend pipeline runs as part of my routine checks. Our Airflow dashboard showed an unusual pattern - our main transformation DAG had executed far more times than it should have.&lt;/p&gt;

&lt;p&gt;Looking closer, I saw the DAG had run 47 times between Saturday morning and Monday. But we only schedule it once per day at 2 AM.&lt;/p&gt;

&lt;p&gt;What caught my attention: every single run was processing December 3rd's data. Not December 4th, 5th, or 6th. Just December 3rd, repeatedly.&lt;/p&gt;

&lt;p&gt;All runs showed as successful. Green status. No failed tasks. The logs showed normal processing - read data, transform it, write to warehouse, mark complete.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Investigation
&lt;/h2&gt;

&lt;p&gt;I checked the obvious things first:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Was someone manually triggering reruns?&lt;/strong&gt; No. The audit logs showed all runs were automatic, triggered by the scheduler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Had the source data changed?&lt;/strong&gt; No. The S3 timestamps showed December 3rd's data hadn't been modified since it was originally created.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Was there a scheduler configuration issue?&lt;/strong&gt; The schedule looked correct: daily at 2 AM.&lt;/p&gt;

&lt;p&gt;Then I noticed something in the run history. The pattern started on Saturday. Our pipeline ran at 2 AM (normal), then again at 4 AM, 6 AM, 8 AM... every two hours through the weekend.&lt;/p&gt;

&lt;p&gt;That's when I realized: these weren't scheduled runs. These were retries.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Background
&lt;/h2&gt;

&lt;p&gt;The previous Friday, we'd deployed a new analytics feature - calculating average transaction values by customer segment. Marketing wanted to track premium customer behavior separately from regular customers.&lt;/p&gt;

&lt;p&gt;The code had been tested thoroughly. We ran it against sample data from the past week. All tests passed. We deployed Friday afternoon.&lt;/p&gt;

&lt;p&gt;What we didn't test: weekend data patterns.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Root Cause
&lt;/h2&gt;

&lt;p&gt;Our pipeline used Airflow's execution date to determine which data partition to process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;execution_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;execution_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;data_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;s3_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3://bucket/data/date=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline had multiple steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read data from S3&lt;/li&gt;
&lt;li&gt;Transform and validate records&lt;/li&gt;
&lt;li&gt;Calculate daily metrics&lt;/li&gt;
&lt;li&gt;Write to warehouse&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 3 is where things broke on weekends.&lt;/p&gt;

&lt;p&gt;Our new metric calculated "average transaction value per customer segment":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Calculate average for our premium customer segment
&lt;/span&gt;&lt;span class="n"&gt;target_customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;customer_segment&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;premium&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;total_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target_customers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;customer_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target_customers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;nunique&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;avg_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total_value&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;customer_count&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked fine on the weekdays we tested:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;December 3rd (Wednesday): 8,500 premium customers. Calculated successfully.&lt;/li&gt;
&lt;li&gt;December 4th (Thursday): 7,200 premium customers. Calculated successfully.&lt;/li&gt;
&lt;li&gt;December 5th (Friday): 6,800 premium customers. Calculated successfully.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;December 6th (Saturday): 0 premium customers.&lt;/p&gt;

&lt;p&gt;Our premium segment was entirely B2B customers - business accounts, enterprise clients. They don't transact on weekends. The businesses are closed.&lt;/p&gt;

&lt;p&gt;We had plenty of regular consumer transactions on Saturday (48,000 total), but zero from the premium segment we were calculating metrics for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;customer_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target_customers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;nunique&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Returns 0
&lt;/span&gt;&lt;span class="n"&gt;avg_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total_value&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;  &lt;span class="c1"&gt;# Division by zero error
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The calculation failed. Task failed. Airflow scheduled a retry.&lt;/p&gt;

&lt;p&gt;Here's where the bug was. We had retry logic that tried to be helpful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;task_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;try_number&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# If this is a retry, process the last successful date
&lt;/span&gt;    &lt;span class="c1"&gt;# to avoid reprocessing potentially corrupted data
&lt;/span&gt;    &lt;span class="n"&gt;last_successful&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_last_successful_date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;data_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last_successful&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;data_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logic made sense when we wrote it: if a task fails partway through processing, don't try to reprocess potentially corrupted data. Instead, go back to the last known good date.&lt;/p&gt;

&lt;p&gt;But in this case:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;December 6th processing failed (division by zero - no premium customers)&lt;/li&gt;
&lt;li&gt;Retry triggered, using execution_date = December 6th&lt;/li&gt;
&lt;li&gt;Retry logic checked: last successful date = December 3rd&lt;/li&gt;
&lt;li&gt;Processed December 3rd data (which had premium customer transactions)&lt;/li&gt;
&lt;li&gt;Calculation succeeded!&lt;/li&gt;
&lt;li&gt;Airflow marked December 6th as complete&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then the same thing happened with December 7th (Sunday). And continued through the weekend until I stopped it Monday morning.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Impact
&lt;/h2&gt;

&lt;p&gt;The immediate problem was duplicate data. We'd loaded December 3rd's transactions into our warehouse 47 times.&lt;/p&gt;

&lt;p&gt;Our deduplication logic caught most of it - we used transaction IDs as primary keys, so the database just overwrote the same records.&lt;/p&gt;

&lt;p&gt;But not all our downstream reports deduplicated. Some aggregation tables counted each load as new data. For a few hours Monday morning, our dashboards showed December 3rd with 47x normal transaction volume.&lt;/p&gt;

&lt;p&gt;The bigger problem: we had no data for December 6th or 7th. The pipeline thought it had processed those dates successfully (because it processed December 3rd instead), so it moved on to December 8th.&lt;/p&gt;

&lt;p&gt;We skipped two days of weekend data without realizing it until a business user asked why our weekend sales reports were blank.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;I fixed two things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, the immediate bug&lt;/strong&gt; - handle zero-count scenarios in calculations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;target_customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;customer_segment&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;premium&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;customer_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target_customers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;nunique&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;customer_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;avg_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target_customers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;customer_count&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# No customers in this segment - set to NULL rather than failing
&lt;/span&gt;    &lt;span class="n"&gt;avg_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Second, the retry logic&lt;/strong&gt; - removed it entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Always process the execution date, regardless of retry count
&lt;/span&gt;&lt;span class="n"&gt;data_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;execution_date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: retries should reprocess the SAME data, not fall back to old data. If there's a real data problem, retrying won't help. If it's a transient issue, retrying the same operation will work.&lt;/p&gt;

&lt;p&gt;For the weekend scenario specifically, I also updated our metrics logic to handle the expected pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Weekend data note: Premium segment (B2B) has zero weekend activity
# This is expected behavior - record NULL for weekend metrics
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Test with realistic data patterns.&lt;/strong&gt; We tested with weekday data because that's what was convenient. We should have tested with weekend data, holiday data, month-end data - all the edge cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retry logic needs careful thought.&lt;/strong&gt; Our retry logic assumed "last successful date" was a safe fallback. It wasn't. Retries should reprocess the same data, not different data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Division by zero is common in analytics.&lt;/strong&gt; Anytime you're calculating averages or ratios, handle the zero-count case explicitly. Don't just let it fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitor successful runs, not just failures.&lt;/strong&gt; All our alerts focused on failures. These runs succeeded, so we had no alerts. The only way I caught it was manually reviewing logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution date vs data date matter.&lt;/strong&gt; Airflow's execution date is when the job runs. The data you process might be different, especially with retries. Keep them separate in your code.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Aftermath
&lt;/h2&gt;

&lt;p&gt;After the fix, the pipeline handled weekend data normally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Saturday: Processed December 13th. Premium metrics = NULL (expected). Success.&lt;/li&gt;
&lt;li&gt;Sunday: Processed December 14th. Premium metrics = NULL (expected). Success.&lt;/li&gt;
&lt;li&gt;No retries. No duplicate processing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I backfilled the missing December 6th and 7th data manually and added a test case for weekend scenarios to our test suite.&lt;/p&gt;

&lt;p&gt;Total time debugging: about 3 hours. Time spent fixing missing weekend data: another 2 hours.&lt;/p&gt;

&lt;p&gt;Lesson learned: always test edge cases, especially predictable ones like weekends.&lt;/p&gt;




&lt;p&gt;Have you deployed code on a Friday that broke over the weekend? Or had retry logic that made things worse instead of better?&lt;/p&gt;

&lt;p&gt;I'd be interested to hear how others handle data quality validation for metrics with variable data patterns.&lt;/p&gt;

&lt;p&gt;Connect with me on &lt;a href="https://linkedin.com/in/pradeepkalluri" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or check out my &lt;a href="https://kalluripradeep.github.io" rel="noopener noreferrer"&gt;portfolio&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks for reading! Follow for more practical data engineering stories and lessons from production systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>airflow</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why 71,000 Data Engineers Read My Article: What I Learned About Technical Writing</title>
      <dc:creator>Pradeep Kalluri</dc:creator>
      <pubDate>Mon, 08 Dec 2025 21:38:19 +0000</pubDate>
      <link>https://dev.to/pradeep_kaalluri/why-71000-data-engineers-read-my-article-what-i-learned-about-technical-writing-1g6a</link>
      <guid>https://dev.to/pradeep_kaalluri/why-71000-data-engineers-read-my-article-what-i-learned-about-technical-writing-1g6a</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqed1b69t6w80upxk52vz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqed1b69t6w80upxk52vz.webp" alt=" " width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My article on data quality hit 71,000 views in a week. I didn't expect that.&lt;/p&gt;

&lt;p&gt;I've been writing technical content for years. Most articles get a few hundred views, maybe a thousand if I'm lucky. This one was different. It sparked 100+ upvotes on Reddit, generated 40+ discussions, and reached engineers at companies from early-stage startups to Fortune 500 banks.&lt;/p&gt;

&lt;p&gt;What made this one work when others didn't? I spent the past week analyzing the engagement, reading every comment, and trying to understand what resonated. Here's what I learned about technical writing that actually gets read.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Wrote About Pain, Not Solutions
&lt;/h2&gt;

&lt;p&gt;Most technical articles start with a solution. "Here's how to implement data quality checks in Spark." "5 ways to optimize your pipeline." "A framework for..."&lt;/p&gt;

&lt;p&gt;My article started with a problem: "Our data pipeline was dropping 10% of transactions and nobody noticed."&lt;/p&gt;

&lt;p&gt;That opening line got more engagement than anything else I've written. Why? Because every data engineer has been there. We've all had that Monday morning panic when someone asks why the numbers look wrong.&lt;/p&gt;

&lt;p&gt;Pain is universal. Solutions are specific.&lt;/p&gt;

&lt;p&gt;When you start with pain, readers think "that's me." When you start with a solution, they think "does this apply to my situation?"&lt;/p&gt;

&lt;p&gt;The most-upvoted comment on my Reddit post: "I felt this in my soul. Currently debugging a similar issue." Not "great solution" or "I'll try this." Just recognition of shared pain.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Showed My Mistakes, Not My Expertise
&lt;/h2&gt;

&lt;p&gt;I could have written "5 Best Practices for Data Quality" and listed industry-standard recommendations. Schema validation. Freshness checks. Data contracts. All correct. All boring.&lt;/p&gt;

&lt;p&gt;Instead, I wrote about the time I deployed validation logic on a Friday afternoon and spent the next week recovering 10% of our transactions from raw storage.&lt;/p&gt;

&lt;p&gt;The difference? Vulnerability.&lt;/p&gt;

&lt;p&gt;When you show expertise, readers feel inadequate. When you show mistakes, they feel understood. The best technical writing doesn't make you look smart - it makes readers feel less alone in their struggles.&lt;/p&gt;

&lt;p&gt;One engineer commented: "Thank you for being honest about this. Most articles make it seem like everyone has perfect pipelines except me."&lt;/p&gt;

&lt;p&gt;That's the response that tells you your writing worked. Not "great tutorial" but "I thought I was the only one."&lt;/p&gt;

&lt;h2&gt;
  
  
  I Used Specific Numbers, Not Vague Examples
&lt;/h2&gt;

&lt;p&gt;Generic: "Data quality issues can cause significant problems."&lt;/p&gt;

&lt;p&gt;Specific: "We dropped 10% of transactions. Finance called. Then my manager called."&lt;/p&gt;

&lt;p&gt;The specificity makes it real. Anyone can write about "quality issues causing problems." But 10%? That's a number someone will remember. That's a number that shows this actually happened.&lt;/p&gt;

&lt;p&gt;Throughout the article, I used real numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10% of data dropped&lt;/li&gt;
&lt;li&gt;Three weekends of being paged at 6 AM&lt;/li&gt;
&lt;li&gt;$100 becoming 10,000 (exactly 100x)&lt;/li&gt;
&lt;li&gt;Revenue reports 3-5% off&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each number grounds the story in reality. Readers commented on specific numbers more than anything else. "The $100 to 10,000 thing happened to us with currency conversion!"&lt;/p&gt;

&lt;p&gt;Vague writing is forgettable. Specific writing is memorable.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Wrote for Skimmers, Not Readers
&lt;/h2&gt;

&lt;p&gt;Most people don't read articles. They skim them.&lt;/p&gt;

&lt;p&gt;I structured every section the same way:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clear heading describing the mistake&lt;/li&gt;
&lt;li&gt;Opening hook - what went wrong&lt;/li&gt;
&lt;li&gt;The debugging journey&lt;/li&gt;
&lt;li&gt;The fix&lt;/li&gt;
&lt;li&gt;The lesson learned&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Readers could skim the headings, pick the mistakes that sounded familiar, and dive into just those sections. Multiple people commented "I skipped to #3 because we just had a currency issue."&lt;/p&gt;

&lt;p&gt;That's not a failure of writing. That's success. They found value without reading every word.&lt;/p&gt;

&lt;p&gt;Good technical writing respects that people are busy. Structure helps them find what they need quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Ended with Questions, Not Conclusions
&lt;/h2&gt;

&lt;p&gt;Most articles end with "In conclusion..." followed by a summary of what you just read.&lt;/p&gt;

&lt;p&gt;I ended with: "What's your worst pipeline debugging story?"&lt;/p&gt;

&lt;p&gt;That question generated half the comments. People shared their own disasters. Currency issues. Schema changes. Data quietly disappearing. Each comment added value for future readers.&lt;/p&gt;

&lt;p&gt;The best technical articles start conversations, not just share information. Questions invite engagement. Conclusions end it.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Published on a Platform Where My Audience Lives
&lt;/h2&gt;

&lt;p&gt;I didn't just post on Medium and hope people found it. I posted directly to r/dataengineering on Reddit, where thousands of data engineers actively discuss their work.&lt;/p&gt;

&lt;p&gt;Platform matters. A brilliant article posted to the wrong platform gets zero views. A decent article posted where your audience already hangs out gets thousands.&lt;/p&gt;

&lt;p&gt;Where does your audience spend time? That's where you should publish first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Didn't Matter
&lt;/h2&gt;

&lt;p&gt;Before the article went viral, I worried about things that turned out not to matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Perfect writing&lt;/strong&gt;: My article has typos. A few sentences are awkwardly phrased. Nobody cared. The content mattered more than the polish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Length&lt;/strong&gt;: The article is long - over 2,000 words. Conventional wisdom says shorter is better. But if the content is valuable, people will read. Several comments said "this was long but worth every minute."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SEO optimization&lt;/strong&gt;: I didn't optimize for search engines. I wrote for humans. The article ranks nowhere in Google but hit 71,000 views through Reddit and LinkedIn shares.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fancy formatting&lt;/strong&gt;: No graphics, no diagrams, no custom styling. Just text, code blocks, and clear headings. Content beats design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Professional polish&lt;/strong&gt;: I wrote how I talk. Contractions, sentence fragments, informal language. It feels more authentic than formal technical writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern Across Popular Technical Content
&lt;/h2&gt;

&lt;p&gt;After analyzing my article's success, I looked at other high-engagement technical posts. A pattern emerged:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They all start with a specific problem the author actually experienced.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not "here's a common issue" but "here's what happened to me last Tuesday."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They all show vulnerability.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not "here's what I know" but "here's what I learned after screwing up."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They all use concrete details.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Numbers, names, specific error messages. Real things that happened to a real person.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They all invite discussion.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They end with questions or acknowledgment that others might have different experiences.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means for Your Technical Writing
&lt;/h2&gt;

&lt;p&gt;If you're writing technical content, here's what worked for me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with a real problem you solved.&lt;/strong&gt; Not a hypothetical scenario. Something that actually cost you time, caused stress, made you look bad.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be specific.&lt;/strong&gt; Use real numbers. Quote actual error messages. Describe the exact commands you ran.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Show the messy middle.&lt;/strong&gt; Don't just show the solution. Show the three wrong paths you took first. The assumptions you made that were wrong. The obvious thing you missed for way too long.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structure for skimmers.&lt;/strong&gt; Clear headings. Predictable section structure. Make it easy to find the parts people care about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write for people, not search engines.&lt;/strong&gt; Informal language. Short paragraphs. Conversational tone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;End with a question.&lt;/strong&gt; Invite people to share their experiences. The comments add as much value as your article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Post where your audience lives.&lt;/strong&gt; Don't just publish and pray. Go to where engineers already discuss problems like yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Unexpected Part
&lt;/h2&gt;

&lt;p&gt;The article's success wasn't about the writing quality. It was about recognizing shared experience.&lt;/p&gt;

&lt;p&gt;Every engineer who upvoted or commented had debugged similar issues. Weekend-only failures. Silent data loss. Schema changes nobody announced. They'd all been there.&lt;/p&gt;

&lt;p&gt;The article worked because it said "you're not alone" better than it said "here's the answer."&lt;/p&gt;

&lt;p&gt;That's probably the most important lesson about technical writing: your readers don't need you to be the smartest person in the room. They need you to be the honest one.&lt;/p&gt;

&lt;p&gt;Write about what went wrong. Show your debugging process, including the dead ends. Use real numbers from real incidents. Be specific enough that people know this actually happened to you.&lt;/p&gt;

&lt;p&gt;That vulnerability creates trust. And trust is what makes people read your writing, share it, and come back for more.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm Writing Next
&lt;/h2&gt;

&lt;p&gt;This experience changed how I think about technical writing. I'm planning more articles about real production incidents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The time our pipeline processed the same day's data 47 times&lt;/li&gt;
&lt;li&gt;When I accidentally made our entire data warehouse read-only&lt;/li&gt;
&lt;li&gt;The monitoring alert that cried wolf so often we turned it off (then regretted it)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one will follow the same pattern: real problem, specific details, messy debugging process, lessons learned.&lt;/p&gt;

&lt;p&gt;Because apparently that's what 71,000 people want to read.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What's your experience with technical writing?&lt;/strong&gt; Have you written content that unexpectedly resonated? Or struggled to get engagement despite great content? I'd love to hear what you've learned.&lt;/p&gt;

&lt;p&gt;Connect with me on &lt;a href="https://linkedin.com/in/pradeepkalluri" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or check out my &lt;a href="https://kalluripradeep.github.io" rel="noopener noreferrer"&gt;portfolio&lt;/a&gt;. Always happy to discuss technical writing, data engineering, or production war stories.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks for reading! If this resonated with you, follow for more articles on data engineering, building in production, and lessons learned the hard way.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>writing</category>
      <category>programming</category>
      <category>career</category>
    </item>
    <item>
      <title>5 Data Pipeline Mistakes That Cost Me Weeks of Debugging</title>
      <dc:creator>Pradeep Kalluri</dc:creator>
      <pubDate>Mon, 01 Dec 2025 19:14:02 +0000</pubDate>
      <link>https://dev.to/pradeep_kaalluri/5-data-pipeline-mistakes-that-cost-me-weeks-of-debugging-2611</link>
      <guid>https://dev.to/pradeep_kaalluri/5-data-pipeline-mistakes-that-cost-me-weeks-of-debugging-2611</guid>
      <description>&lt;p&gt;After three years building data pipelines in production, I’ve made plenty of mistakes. Some were quick fixes. Others cost me days of debugging and awkward conversations with management.&lt;/p&gt;

&lt;p&gt;Here are five mistakes that taught me the most — not because they were dramatic or interesting, but because they were subtle enough to slip through testing and painful enough that I’ll never make them again.&lt;/p&gt;

&lt;p&gt;If you’re building data pipelines, hopefully my mistakes save you some time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #1: Silently Dropping 10% of Data
&lt;/h2&gt;

&lt;p&gt;I added validation logic to filter out “invalid” records from our pipeline. Seemed smart — catch bad data before it reaches the warehouse. I tested it on a sample dataset, everything looked fine, deployed it on a Friday afternoon.&lt;/p&gt;

&lt;p&gt;Monday morning, a business analyst asked why revenue looked 10% lower than expected. My first thought was “probably just a slow weekend.” Then finance called. Then my manager called.&lt;/p&gt;

&lt;p&gt;Turns out, a source system had added a new status code without telling anyone. My validation logic saw this unknown code, flagged those records as invalid, and silently dropped them. 10% of our transactions, gone. The pipeline ran successfully — all green checkmarks, no errors — because technically it worked exactly as I’d coded it.&lt;/p&gt;

&lt;p&gt;The worst part? It took almost a full day to figure out what was wrong. I was looking for pipeline failures, schema mismatches, network issues. Everything checked out. The data was being dropped intentionally by my own code, so there were no error logs.&lt;/p&gt;

&lt;p&gt;How did I fix it? First, I stopped the pipeline immediately. Then I recovered the data from our raw zone and reprocessed it. Took about four hours to backfill everything.&lt;/p&gt;

&lt;p&gt;But the real fix was changing how I think about validation. Now I don’t drop data silently — I send it to an error table with alerts. If something unexpected appears, I know about it. And I never, ever deploy significant changes on Friday afternoons anymore.&lt;/p&gt;

&lt;p&gt;The lesson? Pipelines that succeed aren’t always correct. Green checkmarks just mean your code ran, not that it did the right thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #2: The Weekend Bug That Haunted Me
&lt;/h2&gt;

&lt;p&gt;Our pipeline ran perfectly Monday through Friday. Every single weekend it failed. Every Saturday and Sunday morning, I got paged.&lt;/p&gt;

&lt;p&gt;For the first few weeks, I thought it was a coincidence. Maybe the source system had issues on weekends. Maybe network problems. I spent hours checking infrastructure, reviewing logs, testing connections. Everything looked fine during the week.&lt;/p&gt;

&lt;p&gt;Then I realized — the pipeline wasn’t actually failing. It was being killed by our monitoring system because it thought something was wrong.&lt;/p&gt;

&lt;p&gt;The problem? I had a row count check with a hard-coded threshold. The pipeline expected at least 100,000 records per day. Monday through Friday, we got 120,000–150,000 transactions. Easy pass.&lt;/p&gt;

&lt;p&gt;Weekends? Only 20,000–30,000 transactions. Our customers didn’t work weekends. Lower volume was completely normal. But my check didn’t know that. It saw “only 20,000 rows” and decided the pipeline had failed.&lt;/p&gt;

&lt;p&gt;The fix was embarrassingly simple — change from a fixed threshold to a percentage-based check comparing against the same day of the week historically. Weekends are compared to previous weekends, not to weekdays.&lt;/p&gt;

&lt;p&gt;Took me three weekends of being woken up at 6 AM to figure this out.&lt;/p&gt;

&lt;p&gt;The lesson? Context matters in data validation. What’s normal on Tuesday isn’t normal on Sunday. Your checks need to understand the patterns in your data, not just absolute numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #3: When $100 Became 10,000
&lt;/h2&gt;

&lt;p&gt;Transaction amounts suddenly doubled in our reports overnight. Every single number was exactly 100x what it should have been.&lt;/p&gt;

&lt;p&gt;This one took me almost a full day to debug because the numbers weren’t obviously wrong. A $100 transaction became 10,000. In isolation, that’s a valid transaction amount. Nothing technically broken — no nulls, no errors, schema matched perfectly.&lt;/p&gt;

&lt;p&gt;The breakthrough came when I compared distributions. Average transaction amount had been around $150 for months. Suddenly it was $15,000. That’s when I knew something systemic had changed.&lt;/p&gt;

&lt;p&gt;I traced it back to the source system. They’d changed from sending amounts in dollars ($100.00) to cents (10000). Their reasoning? “Cents are more precise and avoid floating-point issues.” Fair enough. But they didn’t tell anyone.&lt;/p&gt;

&lt;p&gt;My pipeline happily processed the new format. Why wouldn’t it? Numbers are numbers. The schema was still “decimal field for amount.” Technically valid.&lt;/p&gt;

&lt;p&gt;The fix was adding a validation check — if the average transaction amount changes by more than 50% day-over-day, alert someone. Also, I started tracking the ratio of amounts to compare against historical patterns.&lt;/p&gt;

&lt;p&gt;But more importantly, I learned to monitor distributions, not just point values. A value can be individually valid but collectively wrong. If every transaction suddenly costs 100x more, something changed in how the data is formatted, even if the schema stayed the same.&lt;/p&gt;

&lt;p&gt;Become a member&lt;br&gt;
The lesson? Data can be technically correct but business incorrect. Schema validation catches structure problems. Distribution monitoring catches semantic problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #4: The Schema Change Nobody Told Me About
&lt;/h2&gt;

&lt;p&gt;A source system added new columns to their schema without telling anyone. I didn’t update my transformation logic to include these columns when checking for duplicates.&lt;/p&gt;

&lt;p&gt;The result? Records that should have been deduplicated weren’t. We started seeing the same transactions appear multiple times in our reports. Not every record — just enough to make the numbers look slightly off.&lt;/p&gt;

&lt;p&gt;The confusing part was that my deduplication logic was working correctly for the old schema. I was using &lt;code&gt;transaction_id&lt;/code&gt; and &lt;code&gt;timestamp&lt;/code&gt; to identify duplicates. But the source system had added a &lt;code&gt;version&lt;/code&gt; column that changed for retries. Same &lt;code&gt;transaction_id&lt;/code&gt;, same &lt;code&gt;timestamp&lt;/code&gt;, different &lt;code&gt;version&lt;/code&gt;. My code saw them as the same record. The database saw them as different.&lt;/p&gt;

&lt;p&gt;It took me two days to figure out because the duplicates weren’t obvious. Revenue reports were 3–5% higher than expected. Not enough to scream “something’s broken” but enough that finance noticed during reconciliation.&lt;/p&gt;

&lt;p&gt;The fix was simple once I found it — include all relevant columns in the deduplication logic. The lesson? Always check what changed in the source schema, even if nobody tells you it changed.&lt;/p&gt;

&lt;p&gt;Now I log schema changes automatically. If a new column appears, I get an alert. Saves me from assuming the schema is the same as last week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #5: The Missing Columns in COALESCE
&lt;/h2&gt;

&lt;p&gt;I was merging data from multiple sources using COALESCE to pick the first non-null value across columns. Simple enough — if Source A has the data, use it. If not, fall back to Source B, then Source C.&lt;/p&gt;

&lt;p&gt;Except I didn’t include all the columns in my logic. I focused on the main fields — customer ID, transaction amount, date. But I missed some metadata columns like &lt;code&gt;source_system_id&lt;/code&gt; and &lt;code&gt;updated_timestamp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This created duplicates because records that should have been identified as the same transaction weren’t. They had the same main fields but different metadata, so my join logic treated them as separate records.&lt;/p&gt;

&lt;p&gt;Debugging this was frustrating because the duplicates followed no obvious pattern. Some customers had them, others didn’t. Some days had duplicates, other days were clean. It looked random.&lt;/p&gt;

&lt;p&gt;The breakthrough came when I added granularity to my debugging — instead of just checking if duplicates existed, I checked exactly which columns were causing them. I wrote a query that compared all fields between duplicate records and showed me which ones differed.&lt;/p&gt;

&lt;p&gt;That’s when I saw it — the metadata columns I’d ignored. Once I added them to my COALESCE logic with proper priority ordering, the duplicates disappeared.&lt;/p&gt;

&lt;p&gt;The lesson? When handling multiple data sources, think about ALL columns that define uniqueness, not just the obvious business keys. And when debugging duplicates, check field-by-field to see exactly what’s different.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Looking back at these five mistakes, there’s a pattern:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test the right things.&lt;/strong&gt; Schema validation is easy. Testing business logic is hard. Most of my bugs came from assumptions about the data, not the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitor what matters.&lt;/strong&gt; Green checkmarks mean your pipeline ran. They don’t mean your data is correct. Track distributions, row counts, and patterns — not just success/failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context is everything.&lt;/strong&gt; A valid value on Tuesday might be invalid on Sunday. A normal schema last week might have changed this week. Your validation logic needs to understand context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never drop data silently.&lt;/strong&gt; If something looks wrong, flag it loudly. Send it to an error table. Alert someone. Don’t just filter it out and hope it was actually bad data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep raw data.&lt;/strong&gt; Every single one of these mistakes was fixable because we kept the original data. When your transformation logic is wrong, you can reprocess. When you’ve dropped the raw data, you’re done.&lt;/p&gt;

&lt;p&gt;The best part about making mistakes? You only make each one once — if you learn from it. These five cost me weeks of debugging time. But now I have checks in place to catch them before they reach production.&lt;/p&gt;

&lt;p&gt;What’s your worst pipeline debugging story? I’d love to hear what others have learned the hard way.&lt;/p&gt;

&lt;p&gt;— -&lt;/p&gt;

&lt;p&gt;— -&lt;/p&gt;

&lt;p&gt;Want to discuss data pipelines or debugging strategies? Connect with me on &lt;a href="https://linkedin.com/in/pradeepkalluri" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or check out my &lt;a href="https://kalluripradeep.github.io" rel="noopener noreferrer"&gt;portfolio&lt;/a&gt;. Always happy to talk about building reliable data systems.&lt;/p&gt;

&lt;p&gt;— -&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading! If this was helpful, follow for more articles on data engineering, production lessons, and building reliable systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>python</category>
      <category>datascience</category>
      <category>devbugsmash</category>
    </item>
    <item>
      <title>Data Quality at Scale: Why Your Pipeline Needs More Than Green Checkmarks</title>
      <dc:creator>Pradeep Kalluri</dc:creator>
      <pubDate>Mon, 24 Nov 2025 10:02:15 +0000</pubDate>
      <link>https://dev.to/pradeep_kaalluri/data-quality-at-scale-why-your-pipeline-needs-more-than-green-checkmarks-2dch</link>
      <guid>https://dev.to/pradeep_kaalluri/data-quality-at-scale-why-your-pipeline-needs-more-than-green-checkmarks-2dch</guid>
      <description>&lt;p&gt;Originally published on Medium: &lt;a href="https://medium.com/@kalluripradeep99/data-quality-at-scale-why-your-pipeline-needs-more-than-green-checkmarks-f3af3dbff8a4" rel="noopener noreferrer"&gt;https://medium.com/@kalluripradeep99/data-quality-at-scale-why-your-pipeline-needs-more-than-green-checkmarks-f3af3dbff8a4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Data Quality at Scale: Why Your Pipeline Needs More Than Green Checkmarks&lt;br&gt;
I once watched a company make a major strategic decision based on a dashboard that had been showing incorrect data for three weeks. The scary part? Nobody knew. The data pipeline ran successfully every day. All green checkmarks in Airflow. Zero alerts. Everything looked fine.&lt;br&gt;
Except the data was wrong.&lt;br&gt;
After years of building data platforms, I've learned something important: moving data is the easy part. Making sure it's correct is what keeps you up at night.&lt;br&gt;
In this article, I'll share what I've learned about data quality at scale. Not the theory you read in textbooks, but the practical stuff that actually matters when your CEO is looking at a dashboard you built.&lt;br&gt;
The $2 Million Dashboard&lt;br&gt;
Let me tell you about that incident I mentioned. A source system quietly changed how they tracked customer IDs. They sent us an email about it (that got lost in someone's inbox). Our pipeline kept running perfectly. Schema matched. No null values. Everything technically valid.&lt;br&gt;
But we were now double-counting about 15% of customers.&lt;br&gt;
For three weeks, our growth metrics looked amazing. Leadership loved it. They approved a massive marketing spend based on those numbers. Then someone in finance noticed the discrepancy during a reconciliation. We had to go back and explain that our "amazing growth" was actually a data bug.&lt;br&gt;
That was expensive. Not just the money, but the trust. It took months to rebuild confidence in our data platform.&lt;br&gt;
Why Traditional Testing Isn't Enough&lt;br&gt;
If you're coming from software engineering, you might think, "Just write unit tests!" I thought that too. Didn't work.&lt;br&gt;
Here's the thing: with code, you control the inputs. You write tests for expected scenarios. Code is deterministic.&lt;br&gt;
Data is different. You don't control the source systems. They change without telling you. Business rules evolve. Schema drift happens. And here's the worst part - data can be technically valid but business invalid.&lt;br&gt;
Some real examples I've seen:&lt;br&gt;
The string that wasn't a string: Transaction amounts came through as "1,234.56" instead of 1234.56. Schema said "string field," so it passed validation. Try summing those in a SQL query. You get $0.&lt;br&gt;
The date that wasn't wrong: A source system started sending dates in DD/MM/YYYY format instead of MM/DD/YYYY. Every date from the 1st to the 12th of the month worked fine. Then on the 13th, everything broke. Took us two weeks to figure out why.&lt;br&gt;
The midnight ghost records: Mobile app transactions synced when users had WiFi. Some took 48 hours to arrive. Our daily reports were always incomplete, but we had no way to know which days were "final."&lt;br&gt;
I learned the hard way that you need to test six things:&lt;/p&gt;

&lt;p&gt;Schema (structure is right)&lt;br&gt;
Values (numbers make sense)&lt;br&gt;
Volume (right amount of data)&lt;br&gt;
Freshness (data is recent enough)&lt;br&gt;
Distribution (patterns look normal)&lt;br&gt;
Relationships (foreign keys work)&lt;/p&gt;

&lt;p&gt;Most teams only test the first one.&lt;br&gt;
What Data Quality Actually Means&lt;br&gt;
I organize quality checks into four categories. Each one catches different types of problems.&lt;br&gt;
Completeness: Is Everything There?&lt;br&gt;
This seems obvious, but it's where most issues start. You expect 100,000 rows. You get 60,000. Is that a problem or just a slow day?&lt;br&gt;
I check:&lt;/p&gt;

&lt;p&gt;Row counts against historical averages (alert if &amp;gt;20% different)&lt;br&gt;
Null rates in critical fields (customer_id should never be null)&lt;br&gt;
All expected dates/partitions are present&lt;br&gt;
Foreign keys exist (every transaction has a valid customer)&lt;/p&gt;

&lt;p&gt;One time we lost an entire day of data because the source system had a disk space issue. They dumped empty files to our S3 bucket. Our pipeline happily processed zero rows. Everything succeeded. We only found out when someone asked why yesterday's revenue was $0.&lt;br&gt;
Now I check row counts. If today is 50% lower than the average of the last 7 days, I get paged.&lt;br&gt;
Accuracy: Is the Data Correct?&lt;br&gt;
This is harder because "correct" depends on business context. A $1 million transaction might be valid for some businesses, fraud for others.&lt;br&gt;
I focus on:&lt;/p&gt;

&lt;p&gt;Range checks (transaction amounts between $0 and $100,000)&lt;br&gt;
Format validation (emails look like emails, dates are dates)&lt;br&gt;
Business rules (refund amount can't exceed original purchase)&lt;br&gt;
Reconciliation with source systems (row counts and totals match)&lt;/p&gt;

&lt;p&gt;The trick is working with business users to define what "correct" means. Don't guess. Ask.&lt;br&gt;
Consistency: Does It All Make Sense Together?&lt;br&gt;
Data doesn't exist in isolation. Tables relate to each other. Metrics calculated different ways should match.&lt;br&gt;
I check for:&lt;/p&gt;

&lt;p&gt;Orphaned records (transactions without a customer)&lt;br&gt;
Duplicate primary keys (should be impossible but happens)&lt;br&gt;
Cross-table consistency (revenue calculated two ways gives same answer)&lt;br&gt;
Time-series anomalies (revenue doesn't drop 90% overnight unless something big happened)&lt;/p&gt;

&lt;p&gt;We once had a bug where a retry mechanism created duplicate records. Started at 0.1%. Grew to 15% over three months. Aggregations were inflated. We were reporting 15% higher revenue than we actually had. Found it during a financial audit. Not fun.&lt;br&gt;
Freshness: Is It Recent Enough?&lt;br&gt;
Stale data is useless data. But "fresh enough" depends on the use case. Real-time fraud detection needs data from the last minute. Monthly reports can tolerate day-old data.&lt;br&gt;
I monitor:&lt;/p&gt;

&lt;p&gt;Maximum timestamp in each table&lt;br&gt;
Time since last successful pipeline run&lt;br&gt;
SLA breaches (data should be &amp;lt;2 hours old for dashboards)&lt;/p&gt;

&lt;p&gt;Set clear SLAs. Measure against them. Alert when you miss them.&lt;br&gt;
How I Actually Implement This&lt;br&gt;
Theory is nice. Let me show you what I actually do.&lt;br&gt;
Great Expectations for Processing Layer&lt;br&gt;
This tool changed how I think about data quality. Instead of writing custom validation code, you define expectations. Then run them automatically.&lt;br&gt;
Here's a real example from a transaction pipeline:&lt;br&gt;
pythonimport great_expectations as ge&lt;/p&gt;

&lt;h1&gt;
  
  
  Load your data
&lt;/h1&gt;

&lt;p&gt;df = ge.read_csv("s3://curated-zone/transactions.csv")&lt;/p&gt;

&lt;h1&gt;
  
  
  Critical checks - pipeline stops if these fail
&lt;/h1&gt;

&lt;p&gt;df.expect_column_values_to_not_be_null("transaction_id")&lt;br&gt;
df.expect_column_values_to_be_unique("transaction_id")&lt;br&gt;
df.expect_column_values_to_not_be_null("customer_id")&lt;/p&gt;

&lt;h1&gt;
  
  
  Range checks
&lt;/h1&gt;

&lt;p&gt;df.expect_column_values_to_be_between("amount", min_value=0, max_value=1000000)&lt;/p&gt;

&lt;h1&gt;
  
  
  Valid values only
&lt;/h1&gt;

&lt;p&gt;df.expect_column_values_to_be_in_set("currency", ["USD", "EUR", "GBP"])&lt;br&gt;
df.expect_column_values_to_be_in_set("status", ["pending", "completed", "failed"])&lt;/p&gt;

&lt;h1&gt;
  
  
  Format validation
&lt;/h1&gt;

&lt;p&gt;df.expect_column_values_to_match_regex(&lt;br&gt;
    "email",&lt;br&gt;
    r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$"&lt;br&gt;
)&lt;/p&gt;

&lt;h1&gt;
  
  
  Business rule: refunds can't exceed original amount
&lt;/h1&gt;

&lt;p&gt;df.expect_column_pair_values_A_to_be_greater_than_B(&lt;br&gt;
    column_A="original_amount",&lt;br&gt;
    column_B="refund_amount"&lt;br&gt;
)&lt;/p&gt;

&lt;h1&gt;
  
  
  Row count check (based on historical average)
&lt;/h1&gt;

&lt;p&gt;df.expect_table_row_count_to_be_between(&lt;br&gt;
    min_value=50000,&lt;br&gt;
    max_value=200000&lt;br&gt;
)&lt;/p&gt;

&lt;h1&gt;
  
  
  Run all checks
&lt;/h1&gt;

&lt;p&gt;results = df.validate()&lt;/p&gt;

&lt;p&gt;if not results.success:&lt;br&gt;
    failed = [exp for exp in results.results if not exp.success]&lt;br&gt;
    print(f"Quality check failed! {len(failed)} issues found")&lt;br&gt;
    # Send alert, stop pipeline, whatever makes sense&lt;br&gt;
    raise ValueError("Data quality check failed")&lt;br&gt;
I run this in my Airflow pipeline right after reading from the raw zone. If validation fails, the pipeline stops. Bad data never reaches production.&lt;br&gt;
The key is starting simple. Five checks on day one. Add more as you learn what can go wrong. I now have 50+ checks on critical tables. Built up over time based on actual incidents.&lt;br&gt;
dbt Tests for Analytics Layer&lt;br&gt;
While Great Expectations handles the processing layer, I use dbt for the warehouse. Tests live right next to the models. Easy for analysts to write and maintain.&lt;br&gt;
yaml# models/schema.yml&lt;br&gt;
version: 2&lt;/p&gt;

&lt;p&gt;models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name: fct_daily_revenue
description: "Daily revenue by product"
columns:

&lt;ul&gt;
&lt;li&gt;name: date
tests:

&lt;ul&gt;
&lt;li&gt;not_null&lt;/li&gt;
&lt;li&gt;unique&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;name: product_id
tests:

&lt;ul&gt;
&lt;li&gt;not_null&lt;/li&gt;
&lt;li&gt;relationships:
  to: ref('dim_products')
  field: product_id&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;name: revenue
tests:

&lt;ul&gt;
&lt;li&gt;not_null&lt;/li&gt;
&lt;li&gt;dbt_utils.accepted_range:
  min_value: 0
  max_value: 10000000
And custom tests for business logic:
sql-- tests/revenue_reconciliation.sql
-- Revenue in warehouse should match source system&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;with warehouse as (&lt;br&gt;
    select sum(revenue) as total&lt;br&gt;
    from {{ ref('fct_daily_revenue') }}&lt;br&gt;
    where date = current_date - 1&lt;br&gt;
),&lt;/p&gt;

&lt;p&gt;source as (&lt;br&gt;
    select total_revenue as total&lt;br&gt;
    from {{ ref('source_summary') }}&lt;br&gt;
    where date = current_date - 1&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;select *&lt;br&gt;
from warehouse w&lt;br&gt;
cross join source s&lt;br&gt;
where abs(w.total - s.total) / s.total &amp;gt; 0.01  -- Fail if &amp;gt;1% difference&lt;br&gt;
Run these after every model build. If tests fail, you know immediately.&lt;br&gt;
Monitoring and Alerts&lt;br&gt;
Quality checks are useless if nobody looks at them. You need alerts that actually get attention.&lt;br&gt;
I use three severity levels:&lt;br&gt;
Critical (page someone):&lt;/p&gt;

&lt;p&gt;Pipeline completely failed&lt;br&gt;
Zero rows loaded&lt;br&gt;
SLA breach by &amp;gt;4 hours&lt;/p&gt;

&lt;p&gt;High (Slack with @channel):&lt;/p&gt;

&lt;p&gt;Quality checks failed&lt;br&gt;
Volume drop &amp;gt;50%&lt;br&gt;
Freshness breach by &amp;gt;2 hours&lt;/p&gt;

&lt;p&gt;Medium (Slack notification):&lt;/p&gt;

&lt;p&gt;Warning-level checks failed&lt;br&gt;
Volume drop 20-50%&lt;br&gt;
Minor anomalies&lt;/p&gt;

&lt;p&gt;Don't alert on everything. Alert fatigue is real. I learned this by setting alerts too aggressively and then ignoring them. Start conservative. Tune based on false positives.&lt;br&gt;
Building a Quality Culture&lt;br&gt;
Here's what I've learned about getting teams to care about quality:&lt;br&gt;
Show the impact. Don't say "we need more tests." Say "last month's incorrect dashboard cost us a $2M budgeting mistake. These tests prevent that."&lt;br&gt;
Make it visible. We have a dashboard showing data quality scores for every table. Updates daily. Everyone can see it. When scores drop, people notice.&lt;br&gt;
Make it easy. Pre-built test templates. Clear documentation. If adding quality checks is hard, people won't do it.&lt;br&gt;
Celebrate wins. "Zero quality incidents this month!" matters. Recognize teams that maintain high quality scores.&lt;br&gt;
Share incidents. When things break (and they will), do a blameless post-mortem. What happened? What did we learn? How do we prevent it? Share these widely. Learn from mistakes.&lt;br&gt;
The Quality Checklist&lt;br&gt;
Before any new pipeline goes to production, I make sure:&lt;/p&gt;

&lt;p&gt;Schema validation exists&lt;br&gt;
Critical fields have null checks&lt;br&gt;
Value ranges are validated&lt;br&gt;
Row count checks are in place&lt;br&gt;
Freshness monitoring configured&lt;br&gt;
Alerts set up and tested&lt;br&gt;
Team knows how to respond to alerts&lt;br&gt;
Runbook exists for common failures&lt;/p&gt;

&lt;p&gt;Takes 30 minutes to set up. Saves hours when things break.&lt;br&gt;
Real Incidents I've Seen&lt;br&gt;
Let me share three more incidents and what I learned from each.&lt;br&gt;
The silent schema change: A source system added a new status code without telling us. Our pipeline treated it as invalid and dropped those records. 10% of data quietly disappeared. We found out when a business user asked why certain transactions weren't showing up.&lt;br&gt;
Lesson: Monitor unexpected values. If a new status code appears, alert on it. Don't silently drop data.&lt;br&gt;
The weekend bug: Our pipeline ran fine Monday through Friday. Every weekend it failed. Why? Because weekend volume was 80% lower. Our row count check had a fixed threshold, not a relative one. Every Sunday morning, someone got paged.&lt;br&gt;
Lesson: Make thresholds context-aware. Weekend expectations ≠ weekday expectations.&lt;br&gt;
The currency confusion: Transaction amounts suddenly doubled. Took us a day to figure out why. A source system changed from sending amounts in dollars to cents. $100.00 became 10000. Technically valid (still a number), but wrong.&lt;br&gt;
Lesson: Compare against historical distributions. If average transaction amount suddenly changes by 100x, something's wrong.&lt;br&gt;
What Actually Matters&lt;br&gt;
After years of doing this, here's what I've learned:&lt;br&gt;
Start simple. Five good checks beat 50 mediocre ones. Focus on what actually breaks in your pipelines.&lt;br&gt;
Monitor trends, not just values. A gradual increase in null rates is harder to catch than a sudden spike. Watch the trends.&lt;br&gt;
Test what you can't see. Schema and row counts are easy. Business logic is hard. Both matter.&lt;br&gt;
Make quality everyone's job. Data engineers build the checks. Analysts write tests for their models. Business users define what "correct" means. Shared responsibility works.&lt;br&gt;
Learn from failures. Every incident is a chance to add a test that prevents it from happening again. Build your quality suite from real problems.&lt;br&gt;
Alert strategically. Too many alerts and people ignore them. Too few and you miss real issues. Tune constantly.&lt;br&gt;
The goal isn't perfection. It's trust. When someone looks at a dashboard, they should trust the numbers. When leadership makes a decision based on data, it should be the right decision.&lt;br&gt;
That's what data quality is really about.&lt;br&gt;
Getting Started&lt;br&gt;
If you're building a data platform or trying to improve an existing one:&lt;/p&gt;

&lt;p&gt;Pick your most critical table&lt;br&gt;
Add five basic checks (not null, unique, value ranges, row count, freshness)&lt;br&gt;
Set up alerts&lt;br&gt;
Wait for something to break&lt;br&gt;
Add a check that would have caught it&lt;br&gt;
Repeat&lt;/p&gt;

&lt;p&gt;Don't try to build perfect quality checks on day one. Build them incrementally based on what actually goes wrong.&lt;br&gt;
And when something does break (it will), treat it as a learning opportunity. What check would have caught this? Add it. Move on.&lt;br&gt;
Data quality isn't a project with an end date. It's ongoing vigilance. The teams that do it well make it part of their culture, not just a checklist.&lt;/p&gt;

&lt;p&gt;Want to discuss data quality or pipeline architecture? Connect with me on LinkedIn or check out my portfolio. Always happy to talk about building reliable data systems.&lt;br&gt;
And if you're working on data quality tools or have war stories to share, I'd love to hear them!&lt;/p&gt;

&lt;p&gt;Thanks for reading! If this was helpful, follow for more articles on data engineering, building reliable systems, and lessons learned from production incidents.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>dataquality</category>
      <category>bigdata</category>
      <category>python</category>
    </item>
    <item>
      <title>From Raw to Refined: Data Pipeline Architecture at Scale</title>
      <dc:creator>Pradeep Kalluri</dc:creator>
      <pubDate>Sat, 22 Nov 2025 20:44:40 +0000</pubDate>
      <link>https://dev.to/pradeep_kaalluri/from-raw-to-refined-data-pipeline-architecture-at-scale-119l</link>
      <guid>https://dev.to/pradeep_kaalluri/from-raw-to-refined-data-pipeline-architecture-at-scale-119l</guid>
      <description>&lt;p&gt;Originally published on Medium: &lt;a href="https://medium.com/@kalluripradeep99/from-raw-to-refined-data-pipeline-architecture-at-scale-52cd4b02ef10" rel="noopener noreferrer"&gt;https://medium.com/@kalluripradeep99/from-raw-to-refined-data-pipeline-architecture-at-scale-52cd4b02ef10&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;How I built production data pipelines that process massive volumes daily — and what I learned along the way&lt;/p&gt;

&lt;p&gt;Every day, modern data platforms handle hundreds of gigabytes of data — transactions, customer activity, event streams, operational reports. All of this needs to flow from messy source systems into clean, reliable tables that teams can use for dashboards, reports, and ML models.&lt;/p&gt;

&lt;p&gt;Here’s what surprised me after years of building these systems: moving data isn’t the hard part. Making it reliable at scale is.&lt;/p&gt;

&lt;p&gt;I’ve debugged pipelines that silently corrupted data for weeks. I’ve seen duplicate records inflate ML model accuracy by double digits. I’ve watched pipelines grind to a halt because someone forgot to partition a table properly.&lt;/p&gt;

&lt;p&gt;These experiences taught me something valuable: you need a solid architecture before you write a single line of code.&lt;/p&gt;

&lt;p&gt;In this article, I’ll walk you through the three-zone framework I use for production data pipelines. We’ll cover which tools make sense at each stage, how to keep data quality high, and the mistakes I’ve made so you don’t have to.&lt;/p&gt;

&lt;p&gt;If you’re building a data platform from scratch or trying to scale an existing one, this should help.&lt;/p&gt;

&lt;p&gt;The Three-Zone Architecture&lt;br&gt;
I like keeping things simple. Split your data pipeline into three zones, each doing one thing well. This makes everything easier to build, fix, and explain to your team.&lt;/p&gt;

&lt;p&gt;Zone 1: Raw/Landing Zone&lt;br&gt;
This is where data first shows up. The most important rule: don’t touch it. Store everything exactly as it comes in.&lt;/p&gt;

&lt;p&gt;What it does: Keeps data in its original form&lt;br&gt;
Tools I use: Object storage (S3/ADLS) for batch files, Kafka for streaming&lt;br&gt;
Why it matters: You can always go back and reprocess if something breaks&lt;/p&gt;

&lt;p&gt;Example: Transaction data comes in as JSON files. I store them in organized paths like s3://raw-zone/transactions/2024/11/20/. For real-time data like payment events, they go into Kafka topics unchanged.&lt;/p&gt;

&lt;p&gt;Why bother with this separation? Because you’ll have bugs. Business rules will change. Data quality checks will evolve. When that happens, you just reprocess from raw. It’s your safety net.&lt;/p&gt;

&lt;p&gt;I once discovered a data transformation bug that had been running for weeks. Because we had the raw zone, we reprocessed everything in a few hours. Without it? We would have had serious data integrity issues.&lt;/p&gt;

&lt;p&gt;Zone 2: Curated/Staging Zone&lt;br&gt;
This is where the actual work happens. Clean up the mess, standardize formats, catch bad data before it reaches production.&lt;/p&gt;

&lt;p&gt;What it does: Turns raw data into something usable&lt;br&gt;
Tools I use: PySpark for heavy lifting, cloud compute platforms for processing&lt;br&gt;
What I do here: Remove duplicates, fix data types, validate everything, standardize formats&lt;/p&gt;

&lt;p&gt;Real talk: data is always messier than you expect. You’ll get duplicate records. Date formats all over the place — some systems use MM/DD/YYYY, others use DD-MM-YYYY. Codes that don’t match standards. Nulls everywhere.&lt;/p&gt;

&lt;p&gt;This zone fixes all of that. Convert dates to ISO format. Deduplicate records using window functions. Flag invalid data and send it to error tables so someone can investigate later.&lt;/p&gt;

&lt;p&gt;One time, we received data where the same record appeared multiple times with different values due to system retries. Our deduplication logic caught it and kept only the latest record based on timestamp. Simple, but it prevented incorrect reporting downstream.&lt;/p&gt;

&lt;p&gt;Zone 3: Refined/Consumption Zone&lt;br&gt;
This is what people actually use. Clean, fast, optimized, ready to go.&lt;/p&gt;

&lt;p&gt;What it does: Serves data to analysts, dashboards, ML models&lt;br&gt;
Tools I use: Cloud data warehouses (Snowflake/Redshift/BigQuery), dbt for transformations&lt;br&gt;
What’s here: Star schemas, pre-aggregated tables, feature stores for ML&lt;/p&gt;

&lt;p&gt;Instead of making analysts query millions of raw records, give them pre-aggregated summary tables. Instead of making ML engineers join dozens of tables every time they need features, give them pre-computed feature tables.&lt;/p&gt;

&lt;p&gt;Performance matters here. Use proper partitioning. Pre-compute common aggregations. Model your data in ways people understand — star schemas, not normalized tables with excessive joins.&lt;/p&gt;

&lt;p&gt;Why Split It Up?&lt;br&gt;
Easier to debug: When something breaks, you know exactly where to look. Data quality issue? Check curated. Performance problem? Look at refined.&lt;/p&gt;

&lt;p&gt;Safer to experiment: Want to try a new transformation logic? Test it in curated without touching raw data. Want to change your warehouse schema? Refined zone only.&lt;/p&gt;

&lt;p&gt;Right tool for the job: Object storage for raw, distributed compute for processing, columnar database for analytics. Each zone uses the best tool for its purpose.&lt;/p&gt;

&lt;p&gt;Better quality: Catch problems early in curated before they reach business users and damage trust in your data platform.&lt;/p&gt;

&lt;p&gt;The boundaries are clear: raw-to-curated handles technical stuff (formats, types, duplicates). Curated-to-refined handles business logic (aggregations, joins, metrics). Everyone knows what goes where.&lt;/p&gt;

&lt;p&gt;Data Ingestion Layer&lt;br&gt;
Getting data into your platform is step one. You’ve got two main approaches: batch and streaming. Most real-world systems need both.&lt;/p&gt;

&lt;p&gt;Batch Ingestion&lt;br&gt;
This is your scheduled, bulk data loads. Works great for data that doesn’t need to be real-time — think daily summaries, overnight files, periodic reports.&lt;/p&gt;

&lt;p&gt;I use cloud object storage as the landing zone. Source systems drop files there — usually CSV, JSON, or Parquet. Then I’ve got scheduled jobs (orchestrated by Airflow) that pick them up and process them.&lt;/p&gt;

&lt;p&gt;The trick is organizing your storage paths properly. Use a structure like:&lt;/p&gt;

&lt;p&gt;s3://raw-zone/source_system/table_name/YYYY/MM/DD/filename.parquet&lt;br&gt;
This makes it easy to process specific date ranges and troubleshoot when things go wrong. And trust me, things will go wrong.&lt;/p&gt;

&lt;p&gt;Pro tip: Use Parquet format when you can. Columnar storage can reduce storage costs significantly compared to CSV, plus query performance improves substantially.&lt;/p&gt;

&lt;p&gt;Stream Ingestion&lt;br&gt;
For real-time data, I use Kafka. Payment events, user activity, system logs — anything that needs to be processed within seconds or minutes.&lt;/p&gt;

&lt;p&gt;Kafka is great because it keeps messages for a retention period (say, 7 days). If your downstream system goes down for maintenance, you can catch up without losing data. It’s like a replay buffer for your data streams.&lt;/p&gt;

&lt;p&gt;Here’s a pattern that works well: Kafka producers write events to topics. Consumer applications read from topics and write to object storage in micro-batches (every 5 minutes). This gives you both real-time processing AND a permanent archive in your raw zone.&lt;/p&gt;

&lt;p&gt;In one system, we processed tens of thousands of events per second through Kafka, with consumer lag under a minute. The key was proper partitioning and scaling consumer groups horizontally.&lt;/p&gt;

&lt;p&gt;Handling Late Data&lt;br&gt;
Real-world data doesn’t arrive on time. An event might get recorded but the network hiccups. The data shows up hours late. Or a mobile app was offline and syncs data the next day.&lt;/p&gt;

&lt;p&gt;My rule: always use event time (when it actually happened) not processing time (when you received it). Store both timestamps. This way you can handle late arrivals properly in downstream processing without corrupting your analytics.&lt;/p&gt;

&lt;p&gt;Processing &amp;amp; Transformation&lt;br&gt;
This is where PySpark does the heavy lifting. Reading from raw, applying transformations, writing to curated. Let me show you the patterns that actually work in production.&lt;/p&gt;

&lt;p&gt;Reading from Raw Zone&lt;br&gt;
Start by reading your data. I usually work with Parquet files in object storage because they’re fast and efficient.&lt;/p&gt;

&lt;p&gt;from pyspark.sql import SparkSession&lt;br&gt;
from pyspark.sql.functions import col, to_date, row_number, upper&lt;br&gt;
from pyspark.sql.window import Window&lt;br&gt;
spark = SparkSession.builder \&lt;br&gt;
    .appName("curated_processing") \&lt;br&gt;
    .config("spark.sql.adaptive.enabled", "true") \&lt;br&gt;
    .getOrCreate()&lt;/p&gt;

&lt;h1&gt;
  
  
  Read from raw zone with schema inference
&lt;/h1&gt;

&lt;p&gt;df = spark.read.parquet("s3://raw-zone/transactions/2024/11/20/")&lt;br&gt;
Data Validation&lt;br&gt;
First thing: validate your data. Don’t process garbage.&lt;/p&gt;

&lt;h1&gt;
  
  
  Remove records with null IDs
&lt;/h1&gt;

&lt;p&gt;df_valid = df.filter(col("transaction_id").isNotNull())&lt;/p&gt;

&lt;h1&gt;
  
  
  Check for valid amounts (positive values only)
&lt;/h1&gt;

&lt;p&gt;df_valid = df_valid.filter(col("amount") &amp;gt; 0)&lt;/p&gt;

&lt;h1&gt;
  
  
  Validate date ranges
&lt;/h1&gt;

&lt;p&gt;df_valid = df_valid.filter(&lt;br&gt;
    (col("transaction_date") &amp;gt;= "2024-01-01") &amp;amp;&lt;br&gt;
    (col("transaction_date") &amp;lt;= "2024-12-31")&lt;br&gt;
)&lt;br&gt;
Simple checks like this save you headaches later. Invalid data goes to an error table so someone can investigate — don’t just drop it silently.&lt;/p&gt;

&lt;p&gt;Deduplication&lt;br&gt;
Duplicates are everywhere. Source systems send the same record twice. Networks retry failed requests. It happens constantly.&lt;/p&gt;

&lt;p&gt;Here’s how I handle it — keep the most recent record based on a timestamp:&lt;/p&gt;

&lt;h1&gt;
  
  
  Define window to find duplicates
&lt;/h1&gt;

&lt;p&gt;window = Window.partitionBy("transaction_id") \&lt;br&gt;
               .orderBy(col("timestamp").desc())&lt;/p&gt;

&lt;h1&gt;
  
  
  Keep only the latest record for each transaction_id
&lt;/h1&gt;

&lt;p&gt;df_dedup = df_valid.withColumn("row_num", row_number().over(window)) \&lt;br&gt;
                   .filter(col("row_num") == 1) \&lt;br&gt;
                   .drop("row_num")&lt;br&gt;
This pattern works for any duplicate scenario. Just change the partitionBy and orderBy columns based on your needs. I've used this same logic for customer records, sensor data, and API responses.&lt;/p&gt;

&lt;p&gt;Type Casting and Standardization&lt;br&gt;
Data comes in as strings more often than you’d think. Convert to proper types for downstream processing.&lt;/p&gt;

&lt;h1&gt;
  
  
  Convert string dates to actual dates
&lt;/h1&gt;

&lt;p&gt;df_typed = df_dedup.withColumn(&lt;br&gt;
    "transaction_date",&lt;br&gt;
    to_date(col("date_string"), "yyyy-MM-dd")&lt;br&gt;
)&lt;/p&gt;

&lt;h1&gt;
  
  
  Ensure numeric types with proper precision
&lt;/h1&gt;

&lt;p&gt;df_typed = df_typed.withColumn(&lt;br&gt;
    "amount",&lt;br&gt;
    col("amount").cast("decimal(10,2)")&lt;br&gt;
)&lt;/p&gt;

&lt;h1&gt;
  
  
  Standardize codes to uppercase
&lt;/h1&gt;

&lt;p&gt;df_typed = df_typed.withColumn(&lt;br&gt;
    "currency",&lt;br&gt;
    upper(col("currency"))&lt;br&gt;
)&lt;br&gt;
Writing to Curated Zone&lt;br&gt;
Once data is clean, write it back to storage in the curated zone. Use partitioning for better performance downstream.&lt;/p&gt;

&lt;h1&gt;
  
  
  Write partitioned by date for efficient queries
&lt;/h1&gt;

&lt;p&gt;df_typed.write \&lt;br&gt;
    .mode("overwrite") \&lt;br&gt;
    .partitionBy("transaction_date") \&lt;br&gt;
    .parquet("s3://curated-zone/transactions/")&lt;br&gt;
Partitioning means queries only read relevant data. If someone wants yesterday’s data, Spark only scans yesterday’s partition. Fast and cheap.&lt;/p&gt;

&lt;p&gt;In one pipeline, proper partitioning reduced query times from 45 minutes to just a few minutes. Same data, same query, just better organization.&lt;/p&gt;

&lt;p&gt;Why PySpark?&lt;br&gt;
You might ask — why not just use Pandas? Simple: scale. Pandas runs on one machine’s memory. PySpark distributes across a cluster. When you’re processing large volumes, you need that distributed power.&lt;/p&gt;

&lt;p&gt;Become a member&lt;br&gt;
Plus, PySpark’s lazy evaluation is smart. It optimizes your entire transformation pipeline before executing. Less data shuffling, fewer passes over data, faster results.&lt;/p&gt;

&lt;p&gt;Orchestration with Airflow&lt;br&gt;
You can’t run data jobs manually every day. You need orchestration. Airflow handles scheduling, dependencies, retries, and monitoring — all the operational complexity.&lt;/p&gt;

&lt;p&gt;DAG Design&lt;br&gt;
Here’s a DAG structure for our three-zone pipeline:&lt;/p&gt;

&lt;p&gt;from airflow import DAG&lt;br&gt;
from airflow.operators.python import PythonOperator&lt;br&gt;
from airflow.providers.amazon.aws.sensors.s3 import S3KeySensor&lt;br&gt;
from datetime import datetime, timedelta&lt;br&gt;
default_args = {&lt;br&gt;
    'owner': 'data-team',&lt;br&gt;
    'retries': 2,&lt;br&gt;
    'retry_delay': timedelta(minutes=5),&lt;br&gt;
    'email_on_failure': True,&lt;br&gt;
    'email': ['&lt;a href="mailto:data-alerts@company.com"&gt;data-alerts@company.com&lt;/a&gt;'],&lt;br&gt;
}&lt;br&gt;
dag = DAG(&lt;br&gt;
    'transaction_pipeline',&lt;br&gt;
    default_args=default_args,&lt;br&gt;
    start_date=datetime(2024, 1, 1),&lt;br&gt;
    schedule_interval='@daily',&lt;br&gt;
    catchup=False,&lt;br&gt;
)&lt;/p&gt;

&lt;h1&gt;
  
  
  Task 1: Wait for source data to arrive
&lt;/h1&gt;

&lt;p&gt;wait_for_data = S3KeySensor(&lt;br&gt;
    task_id='wait_for_source_data',&lt;br&gt;
    bucket_name='raw-zone',&lt;br&gt;
    bucket_key='transactions/{{ ds }}/',&lt;br&gt;
    timeout=3600,&lt;br&gt;
    poke_interval=60,&lt;br&gt;
    dag=dag,&lt;br&gt;
)&lt;/p&gt;

&lt;h1&gt;
  
  
  Task 2: Ingest from source to raw
&lt;/h1&gt;

&lt;p&gt;ingest_task = PythonOperator(&lt;br&gt;
    task_id='ingest_to_raw',&lt;br&gt;
    python_callable=ingest_data,&lt;br&gt;
    dag=dag,&lt;br&gt;
)&lt;/p&gt;

&lt;h1&gt;
  
  
  Task 3: Process raw to curated
&lt;/h1&gt;

&lt;p&gt;process_task = PythonOperator(&lt;br&gt;
    task_id='process_to_curated',&lt;br&gt;
    python_callable=process_data,&lt;br&gt;
    dag=dag,&lt;br&gt;
)&lt;/p&gt;

&lt;h1&gt;
  
  
  Task 4: Transform curated to refined
&lt;/h1&gt;

&lt;p&gt;transform_task = PythonOperator(&lt;br&gt;
    task_id='transform_to_refined',&lt;br&gt;
    python_callable=transform_data,&lt;br&gt;
    dag=dag,&lt;br&gt;
)&lt;/p&gt;

&lt;h1&gt;
  
  
  Task 5: Data quality checks
&lt;/h1&gt;

&lt;p&gt;quality_task = PythonOperator(&lt;br&gt;
    task_id='quality_checks',&lt;br&gt;
    python_callable=run_quality_checks,&lt;br&gt;
    dag=dag,&lt;br&gt;
)&lt;/p&gt;

&lt;h1&gt;
  
  
  Set dependencies - clear pipeline flow
&lt;/h1&gt;

&lt;p&gt;wait_for_data &amp;gt;&amp;gt; ingest_task &amp;gt;&amp;gt; process_task &amp;gt;&amp;gt; transform_task &amp;gt;&amp;gt; quality_task&lt;br&gt;
Key Principles&lt;br&gt;
Idempotency: Run the same task twice, get the same result. Use mode("overwrite") with date partitions. If today's job fails and reruns, it overwrites today's data without affecting other days. This is crucial for reliable operations.&lt;/p&gt;

&lt;p&gt;Clear dependencies: The &amp;gt;&amp;gt; operator makes dependencies obvious. Process can't start until ingest finishes. Quality checks run last. Anyone looking at the DAG understands the flow immediately.&lt;/p&gt;

&lt;p&gt;Retry logic: Network hiccups happen. Source systems go down. Airflow retries failed tasks automatically. Set sensible retry counts (2–3) and delays (5–10 minutes).&lt;/p&gt;

&lt;p&gt;Monitoring: Airflow’s UI shows you everything. Which tasks failed? How long did they take? When did they last run? All visible at a glance. I check the dashboard regularly — green boxes mean happy pipelines, red boxes mean I’ve got work to do.&lt;/p&gt;

&lt;p&gt;Alerting: Set up email or Slack alerts for failures. Don’t wait until someone complains about missing data. Know about problems before your users do.&lt;/p&gt;

&lt;p&gt;Data Quality &amp;amp; Validation&lt;br&gt;
Bad data is worse than no data. It leads to wrong decisions, broken dashboards, and lost trust in your platform. I learned this the hard way.&lt;/p&gt;

&lt;p&gt;Why Quality Matters&lt;br&gt;
I once saw an ML model built using data with duplicate IDs. The model performed great in testing — high accuracy. Poor in production — much lower accuracy. Why? Because the duplicates artificially inflated performance metrics during training. It was caught after deployment. Not fun.&lt;/p&gt;

&lt;p&gt;Now I validate everything.&lt;/p&gt;

&lt;p&gt;Using Great Expectations&lt;br&gt;
Great Expectations is my go-to tool for data quality. You define rules (called expectations), and it validates your data against them automatically.&lt;/p&gt;

&lt;p&gt;import great_expectations as ge&lt;/p&gt;

&lt;h1&gt;
  
  
  Load your data as a Great Expectations DataFrame
&lt;/h1&gt;

&lt;p&gt;df = ge.read_csv("s3://curated-zone/transactions.csv")&lt;/p&gt;

&lt;h1&gt;
  
  
  Set expectations - these become tests
&lt;/h1&gt;

&lt;p&gt;df.expect_column_values_to_not_be_null("transaction_id")&lt;br&gt;
df.expect_column_values_to_be_unique("transaction_id")&lt;br&gt;
df.expect_column_values_to_be_between("amount", min_value=0, max_value=1000000)&lt;br&gt;
df.expect_column_values_to_be_in_set("currency", ["USD", "EUR", "GBP"])&lt;br&gt;
df.expect_column_values_to_match_regex("email", r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$")&lt;/p&gt;

&lt;h1&gt;
  
  
  Validate and get results
&lt;/h1&gt;

&lt;p&gt;results = df.validate()&lt;br&gt;
if not results.success:&lt;br&gt;
    # Alert someone, block pipeline, write to error log&lt;br&gt;
    raise ValueError(f"Data quality check failed! {results}")&lt;br&gt;
Simple but effective. If data fails validation, the pipeline stops. No bad data reaches production and corrupts your analytics.&lt;/p&gt;

&lt;p&gt;dbt Tests&lt;br&gt;
For the refined zone, I use dbt tests. They’re built directly into your transformation code, which makes them easy to maintain.&lt;/p&gt;

&lt;p&gt;-- models/daily_summary.sql&lt;br&gt;
{{ config(materialized='table') }}&lt;br&gt;
SELECT&lt;br&gt;
    date,&lt;br&gt;
    customer_id,&lt;br&gt;
    SUM(amount) as total_amount,&lt;br&gt;
    COUNT(*) as transaction_count&lt;br&gt;
FROM {{ ref('transactions') }}&lt;br&gt;
GROUP BY date, customer_id&lt;/p&gt;

&lt;h1&gt;
  
  
  tests/daily_summary.yml
&lt;/h1&gt;

&lt;p&gt;version: 2&lt;br&gt;
models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name: daily_summary
columns:

&lt;ul&gt;
&lt;li&gt;name: customer_id
tests:

&lt;ul&gt;
&lt;li&gt;not_null&lt;/li&gt;
&lt;li&gt;unique&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;name: total_amount
tests:

&lt;ul&gt;
&lt;li&gt;not_null&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;name: date
tests:

&lt;ul&gt;
&lt;li&gt;not_null
tests:&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;dbt_utils.recency:
  datepart: day
  field: date
  interval: 1
dbt runs these tests automatically after building models. If a test fails, you know immediately. The recency test is particularly useful — it alerts you if data stops arriving.&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Continuous Monitoring&lt;br&gt;
Quality isn’t one-and-done. Monitor continuously:&lt;/p&gt;

&lt;p&gt;Track row counts over time: Sudden drops = problem&lt;br&gt;
Watch null rates: If nulls suddenly spike, investigate&lt;br&gt;
Monitor data freshness: Is data arriving on time?&lt;br&gt;
Set up anomaly detection: Catch unusual patterns early&lt;br&gt;
Validate referential integrity: Ensure foreign keys match&lt;br&gt;
I’ve got Slack alerts configured for quality failures. If something breaks overnight, I know about it quickly. Better to know immediately than discover it during a morning meeting.&lt;/p&gt;

&lt;p&gt;Real-World Lessons&lt;br&gt;
Let me share some lessons from systems I’ve built:&lt;/p&gt;

&lt;p&gt;Performance wins:&lt;/p&gt;

&lt;p&gt;Proper partitioning can reduce query times by 10x or more&lt;br&gt;
Parquet format typically reduces storage costs by 60–70% vs CSV&lt;br&gt;
Pre-aggregated tables eliminate the need for complex real-time queries&lt;br&gt;
Reliability improvements:&lt;/p&gt;

&lt;p&gt;The three-zone architecture makes debugging much faster&lt;br&gt;
Comprehensive quality checks catch issues before users see them&lt;br&gt;
Idempotent pipelines allow safe retries without data corruption&lt;br&gt;
Cost optimizations:&lt;/p&gt;

&lt;p&gt;Smart partitioning reduces cloud compute costs significantly&lt;br&gt;
Columnar formats (Parquet) save on both storage and processing&lt;br&gt;
Proper cluster sizing prevents over-provisioning&lt;br&gt;
These results didn’t come from fancy tools or bleeding-edge technology. They came from solid architecture, good practices, and attention to quality.&lt;/p&gt;

&lt;p&gt;Common Mistakes (and How to Avoid Them)&lt;br&gt;
Mistake #1: Skipping the raw zone&lt;br&gt;
“We’ll just clean data as it arrives.” Then you have a bug and no way to reprocess. Always keep raw data.&lt;/p&gt;

&lt;p&gt;Mistake #2: No data quality checks&lt;br&gt;
“We’ll add those later.” Later never comes. Build quality checks from day one.&lt;/p&gt;

&lt;p&gt;Mistake #3: Over-engineering early&lt;br&gt;
You don’t need Kafka and real-time processing for daily batch reports. Start simple, scale when needed.&lt;/p&gt;

&lt;p&gt;Mistake #4: Ignoring monitoring&lt;br&gt;
If you don’t know your pipeline failed, you can’t fix it. Set up alerts and dashboards.&lt;/p&gt;

&lt;p&gt;Mistake #5: Poor partitioning&lt;br&gt;
This kills performance and inflates costs. Partition by date or another high-cardinality field that matches query patterns.&lt;/p&gt;

&lt;p&gt;Mistake #6: Treating all data the same&lt;br&gt;
Not everything needs real-time processing. Batch is cheaper and simpler for most use cases.&lt;/p&gt;

&lt;p&gt;Getting Started&lt;br&gt;
If you’re building a data platform from scratch:&lt;/p&gt;

&lt;p&gt;Start with the three-zone architecture — Even if you’re just moving files around, establish the pattern early&lt;br&gt;
Implement one pipeline end-to-end — Don’t build all the infrastructure first. Get one working pipeline, learn from it&lt;br&gt;
Add quality checks incrementally — Start with basic null checks, expand from there&lt;br&gt;
Monitor everything — Build dashboards and alerts from day one&lt;br&gt;
Document your patterns — Future you (and your team) will thank you&lt;br&gt;
The tools I mentioned — S3/ADLS, Kafka, PySpark, Airflow, Snowflake/Redshift/BigQuery, dbt, Great Expectations — work well together. But they’re not the only options. Use what fits your needs, budget, and team expertise.&lt;/p&gt;

&lt;p&gt;Most importantly: make your pipelines reliable. Teams will depend on them. Analysts will base decisions on the data. Executives will present it to stakeholders. Make sure it’s trustworthy.&lt;/p&gt;

&lt;p&gt;Wrapping Up&lt;br&gt;
Building data pipelines at scale isn’t rocket science, but it requires thought and discipline. The three-zone architecture gives you a solid foundation. Raw for safety, curated for processing, refined for consumption.&lt;/p&gt;

&lt;p&gt;Start simple. One pipeline, end-to-end. Get it working. Add quality checks. Then scale based on actual needs, not hypothetical ones.&lt;/p&gt;

&lt;p&gt;After years and dozens of pipelines, I keep coming back to these patterns because they work. They’re not the newest or the flashiest, but they’re reliable. And in data engineering, reliability beats novelty every time.&lt;/p&gt;

&lt;p&gt;The patterns, code examples, and lessons in this article are all based on real production experience. They’re battle-tested and proven to work at scale. Whether you’re building your first data platform or optimizing an existing one, I hope these insights help you avoid common pitfalls and build something reliable.&lt;/p&gt;

&lt;p&gt;Want to discuss data architecture? Connect with me on LinkedIn or check out my portfolio. I’m always happy to talk about data engineering, pipeline design, or building scalable systems.&lt;/p&gt;

&lt;p&gt;And if you’re contributing to dbt, Airflow, Great Expectations, or other open-source data tools, I’d love to hear about your experiences!&lt;/p&gt;

&lt;p&gt;Thanks for reading! If you found this helpful, consider following for more articles on data engineering, cloud architecture, and building scalable systems.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>bigdata</category>
      <category>python</category>
      <category>dataquality</category>
    </item>
  </channel>
</rss>
