<?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: pawan deore</title>
    <description>The latest articles on DEV Community by pawan deore (@pawandeore).</description>
    <link>https://dev.to/pawandeore</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F431497%2Fc8e8bd82-c7c4-44fb-a613-2723c537fc58.jpg</url>
      <title>DEV Community: pawan deore</title>
      <link>https://dev.to/pawandeore</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pawandeore"/>
    <language>en</language>
    <item>
      <title>Apache Spark Partitioning, Skew, and Broadcast Joins: A Practical Guide</title>
      <dc:creator>pawan deore</dc:creator>
      <pubDate>Sun, 08 Nov 2026 12:15:20 +0000</pubDate>
      <link>https://dev.to/pawandeore/apache-spark-partitioning-skew-and-broadcast-joins-a-practical-guide-1eo</link>
      <guid>https://dev.to/pawandeore/apache-spark-partitioning-skew-and-broadcast-joins-a-practical-guide-1eo</guid>
      <description>&lt;h1&gt;
  
  
  Apache Spark Partitioning, Skew, and Broadcast Joins: A Practical Guide
&lt;/h1&gt;

&lt;p&gt;Apache Spark can process large datasets in parallel because it divides work into &lt;strong&gt;partitions&lt;/strong&gt;. When those partitions are balanced, executors finish work at roughly the same time. When one partition contains far more data than the others, a single task becomes a bottleneck. This is called &lt;strong&gt;data skew&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Partitioning, skew handling, and join strategy are tightly connected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Partitioning determines where records are processed.&lt;/li&gt;
&lt;li&gt;Joins often trigger expensive data shuffles.&lt;/li&gt;
&lt;li&gt;Skew causes a small number of join or aggregation tasks to run much longer than the rest.&lt;/li&gt;
&lt;li&gt;Broadcast joins can avoid shuffling a large fact table when one side is small enough.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This tutorial assumes Spark DataFrames, PySpark, and a typical analytics workload with a large fact table and smaller lookup tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Partitioning: Spark’s Unit of Parallel Work
&lt;/h2&gt;

&lt;p&gt;A Spark DataFrame is split into partitions. Each partition is processed by one task at a time, typically on an executor core.&lt;/p&gt;

&lt;p&gt;For example, a DataFrame with 200 partitions can create up to 200 parallel tasks for a stage. The actual concurrency depends on available executor cores.&lt;/p&gt;

&lt;p&gt;There are several different meanings of “partition” in data engineering, and confusing them causes design mistakes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spark partition&lt;/td&gt;
&lt;td&gt;An in-memory or execution-time chunk of a DataFrame or RDD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shuffle partition&lt;/td&gt;
&lt;td&gt;A partition created after Spark redistributes data for joins, aggregations, or &lt;code&gt;repartition()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File partition&lt;/td&gt;
&lt;td&gt;A physical output directory such as &lt;code&gt;date=2025-03-08/&lt;/code&gt; in Parquet storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table partition&lt;/td&gt;
&lt;td&gt;A logical storage partition defined in a metastore or table format&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A Parquet table partitioned by &lt;code&gt;event_date&lt;/code&gt; can still have poorly balanced Spark partitions during a join. Storage layout and execution layout are related, but they are not the same thing.&lt;/p&gt;

&lt;p&gt;The most important operations are:&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;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repartition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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="nf"&gt;repartition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repartitionByRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_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;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;repartition()&lt;/code&gt; creates a &lt;strong&gt;full shuffle&lt;/strong&gt;. Spark moves rows across executors to produce the requested partitioning.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;coalesce()&lt;/code&gt; usually reduces partitions without a full shuffle. It is useful after filtering data down, but it does not rebalance uneven partitions. If one partition was already huge, &lt;code&gt;coalesce()&lt;/code&gt; can preserve that imbalance.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Joins Create Shuffles
&lt;/h2&gt;

&lt;p&gt;Consider a common pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw order files
    |
    v
Filter and clean orders
    |
    +--------------------+
    |                    |
    v                    v
Large orders table   Small customers table
    |                    |
    +------ join --------+
              |
              v
Aggregate revenue by customer segment
              |
              v
Write Parquet or Delta output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A join between two large DataFrames usually requires Spark to shuffle both sides by the join key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;orders                          customers
  |                                  |
  v                                  v
Exchange hash(customer_id)      Exchange hash(customer_id)
  |                                  |
  +------------- join -------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both tables are distributed by &lt;code&gt;customer_id&lt;/code&gt;, rows for the same customer are placed in the same shuffle partition. That makes the join possible, but it creates a problem when one customer appears far more often than others.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer_id = C_HOT      700 million orders
customer_id = C_001        3 million orders
customer_id = C_002        2 million orders
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All rows for &lt;code&gt;C_HOT&lt;/code&gt; generally land in one hash partition. One task now processes hundreds of millions of rows while other tasks finish quickly.&lt;/p&gt;

&lt;p&gt;This is why adding more executors does not necessarily solve skew. The skewed key still belongs to one partition unless the execution strategy changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Runnable PySpark Example
&lt;/h2&gt;

&lt;p&gt;The following example creates a skewed orders dataset:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One customer, &lt;code&gt;C_HOT&lt;/code&gt;, has 700 orders.&lt;/li&gt;
&lt;li&gt;One hundred normal customers have 3 orders each.&lt;/li&gt;
&lt;li&gt;The customer dimension is small enough to broadcast.&lt;/li&gt;
&lt;li&gt;The expected joined row count is &lt;code&gt;1000&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The expected revenue is &lt;code&gt;13000&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run it with a local Spark installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pyspark
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SparkSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;

&lt;span class="n"&gt;spark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;SparkSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;master&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local[*]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;partitioning-skew-broadcast-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.shuffle.partitions&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;8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.adaptive.enabled&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;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 700 orders from one unusually active customer.
&lt;/span&gt;&lt;span class="n"&gt;hot_orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&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;HOT-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&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;C_HOT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;700&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# 100 normal customers, each with 3 orders.
&lt;/span&gt;&lt;span class="n"&gt;normal_orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&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;NORMAL-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_num&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order_num&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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;C_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer_num&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;customer_num&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;order_num&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;hot_orders&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;normal_orders&lt;/span&gt;&lt;span class="p"&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;order_id&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;customer_id&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;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDataFrame&lt;/span&gt;&lt;span class="p"&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;C_HOT&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;enterprise&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="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;C_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&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;standard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&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;customer_id&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;segment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Force a hash shuffle by customer_id so we can observe skew.
&lt;/span&gt;&lt;span class="n"&gt;orders_by_customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repartition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&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;cache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;orders_by_customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Materialize the cached shuffled DataFrame.
&lt;/span&gt;
&lt;span class="n"&gt;partition_sizes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;orders_by_customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rdd&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mapPartitions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&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="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rows per partition:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;partition_sizes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Largest partition:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;partition_sizes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Broadcast the small dimension table.
&lt;/span&gt;&lt;span class="n"&gt;joined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;orders_by_customer&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&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="n"&gt;how&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;joined&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&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="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;joined_rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;F&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="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;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_revenue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Joined rows:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;joined_rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total revenue:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_revenue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;joined_rows&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="mi"&gt;1000&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_revenue&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="mi"&gt;13000&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;partition_sizes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;700&lt;/span&gt;

&lt;span class="n"&gt;joined&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;explain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formatted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact list of partition sizes varies because Spark’s hash partitioning determines where each customer key lands. However, one partition must contain at least the 700 &lt;code&gt;C_HOT&lt;/code&gt; rows.&lt;/p&gt;

&lt;p&gt;The important result is that the join uses a broadcast strategy for &lt;code&gt;customers&lt;/code&gt;. In the physical plan, look for an operator similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BroadcastHashJoin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spark sends the small &lt;code&gt;customers&lt;/code&gt; table to each executor. The large &lt;code&gt;orders&lt;/code&gt; DataFrame does not need to be shuffled just to perform this join.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Broadcast Joins Help—and When They Do Not
&lt;/h2&gt;

&lt;p&gt;A broadcast join is appropriate when one join input is genuinely small after filtering and selecting only needed columns.&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;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conceptually, Spark does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Large orders partitions        Small customers table
        |                              |
        |                         Broadcast to all executors
        v                              v
Local join in each executor partition
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main benefit is avoiding a shuffle of the large table for the join.&lt;/p&gt;

&lt;p&gt;However, broadcast joins do &lt;strong&gt;not&lt;/strong&gt; solve every skew problem:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A skewed scan remains skewed.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If one input file is much larger than the rest, one scan task may still lag.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A later aggregation can still skew.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Joining orders to customers may be fast, but this can still produce a skewed shuffle:&lt;br&gt;
&lt;/p&gt;&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="n"&gt;joined&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupBy&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;sum&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All &lt;code&gt;C_HOT&lt;/code&gt; rows must still be brought together for that aggregation.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Broadcasting a table that is too large can fail.&lt;/strong&gt;
Every executor needs memory for the broadcast relation. A dimension that looks small in compressed Parquet can expand significantly after deserialization and hashing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Spark can automatically broadcast relations below &lt;code&gt;spark.sql.autoBroadcastJoinThreshold&lt;/code&gt;, but the threshold is configuration-dependent. Explicit &lt;code&gt;broadcast()&lt;/code&gt; is useful when you understand the size and want to communicate intent, not as a blind optimization.&lt;/p&gt;

&lt;p&gt;If you want a broader learning path after practicing these concepts, this optional &lt;a href="https://pavandeore.gumroad.com/l/data-engineering-complete-roadmap-for-beginners" rel="noopener noreferrer"&gt;data engineering roadmap for beginners&lt;/a&gt; can help place Spark optimization topics in context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting and Fixing Data Skew
&lt;/h2&gt;

&lt;p&gt;Start by confirming that skew exists. In the Spark UI, look for a stage where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Most tasks finish quickly.&lt;/li&gt;
&lt;li&gt;One or a few tasks run much longer.&lt;/li&gt;
&lt;li&gt;A few tasks read or write substantially more shuffle data.&lt;/li&gt;
&lt;li&gt;Executor memory pressure or spill occurs only in a few tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also inspect key frequency directly:&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;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupBy&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;truncate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Let Adaptive Query Execution Help First
&lt;/h3&gt;

&lt;p&gt;Adaptive Query Execution (AQE) allows Spark to make some decisions after it observes runtime shuffle statistics.&lt;/p&gt;

&lt;p&gt;Useful settings include:&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;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.adaptive.enabled&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;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.adaptive.skewJoin.enabled&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;true&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;For eligible shuffle joins, Spark can split unusually large shuffle partitions into smaller pieces. AQE can also coalesce tiny shuffle partitions.&lt;/p&gt;

&lt;p&gt;AQE is often the first mitigation to try because it requires no business-logic changes. But it cannot always split work for a single extremely hot key, especially when all records for that key must be processed together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Key Salting for Severe Hot Keys
&lt;/h3&gt;

&lt;p&gt;Salting splits one hot key into multiple artificial keys.&lt;/p&gt;

&lt;p&gt;For example, instead of joining only on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C_HOT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C_HOT_0
C_HOT_1
...
C_HOT_7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fact table assigns a deterministic salt:&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;salt_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;

&lt;span class="n"&gt;salted_orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;salt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&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="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C_HOT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pmod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;salt_count&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;otherwise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dimension table must duplicate only the hot key across the same salt values. Normal customer rows keep salt &lt;code&gt;0&lt;/code&gt;. Then join on both &lt;code&gt;customer_id&lt;/code&gt; and &lt;code&gt;salt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Salting is most useful when both join inputs are too large to broadcast. It adds complexity and duplicates dimension rows, so do not apply it to every key. Identify the small set of problematic keys first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Pitfalls
&lt;/h2&gt;

&lt;p&gt;A few common choices create avoidable Spark performance problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;repartition()&lt;/code&gt; everywhere.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Every repartition is a shuffle. Repartition only when it improves a downstream operation, such as balancing data before a large write or aligning work before a non-broadcast join.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using too many shuffle partitions.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Thousands of tiny tasks add scheduler overhead and can produce small output files. Too few partitions create large tasks, memory pressure, and poor parallelism. Tune based on observed input size, shuffle volume, task duration, and cluster capacity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assuming a broadcast hint is always safe.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A forced broadcast can cause executor out-of-memory failures. Filter dimensions early, select only necessary columns, and verify size under realistic production data volumes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Writing files partitioned by high-cardinality keys.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Writing data partitioned by &lt;code&gt;customer_id&lt;/code&gt; can create massive directory and small-file counts. Storage partitions should usually reflect common pruning filters, such as date, region, or source system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring post-join cardinality.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A duplicate key in a dimension table can turn a one-to-many join into a many-to-many join. This both inflates results and amplifies skew.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Partitioning and Join Behavior
&lt;/h2&gt;

&lt;p&gt;Test correctness before tuning performance.&lt;/p&gt;

&lt;p&gt;For joins, create assertions for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expected row counts&lt;/li&gt;
&lt;li&gt;Expected aggregate totals&lt;/li&gt;
&lt;li&gt;Null handling for unmatched keys&lt;/li&gt;
&lt;li&gt;Duplicate dimension keys&lt;/li&gt;
&lt;li&gt;Known hot-key behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, the runnable example asserts both row count and total revenue. In a real pipeline, add a test where a dimension intentionally contains duplicate &lt;code&gt;customer_id&lt;/code&gt; values and verify that the pipeline rejects or deduplicates them according to your business rule.&lt;/p&gt;

&lt;p&gt;For performance regression testing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build a representative sample containing hot keys.&lt;/li&gt;
&lt;li&gt;Run the job with Spark event logging enabled.&lt;/li&gt;
&lt;li&gt;Compare shuffle read/write volume, task duration distribution, spill metrics, and output row counts.&lt;/li&gt;
&lt;li&gt;Validate the physical plan with &lt;code&gt;explain("formatted")&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Test with production-like executor memory and partition counts; local mode cannot reveal all distributed failure modes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Spark partitioning determines how work is distributed, while joins and aggregations often force data to shuffle. A skewed key can make one partition dominate an entire stage.&lt;/p&gt;

&lt;p&gt;Use broadcast joins when one side is safely small, because they avoid shuffling the large side. Use AQE to handle many skewed shuffle joins automatically. For persistent, severe hot keys that cannot be broadcast, use targeted salting and validate both correctness and resource usage.&lt;/p&gt;

&lt;p&gt;The best optimization is not “more partitions” by default—it is choosing a partitioning and join strategy that matches the actual shape of your data.&lt;/p&gt;

</description>
      <category>bigdata</category>
      <category>data</category>
      <category>performance</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Designing Airflow DAGs with Retries, SLAs, and Idempotency</title>
      <dc:creator>pawan deore</dc:creator>
      <pubDate>Thu, 05 Nov 2026 13:54:44 +0000</pubDate>
      <link>https://dev.to/pawandeore/designing-airflow-dags-with-retries-slas-and-idempotency-4d9</link>
      <guid>https://dev.to/pawandeore/designing-airflow-dags-with-retries-slas-and-idempotency-4d9</guid>
      <description>&lt;h1&gt;
  
  
  Designing Airflow DAGs with Retries, SLAs, and Idempotency
&lt;/h1&gt;

&lt;p&gt;A production Airflow DAG must handle ordinary failure without corrupting data or silently missing its delivery target. Three design concerns work together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Retries&lt;/strong&gt; handle transient failures such as temporary database locks, API rate limits, or network interruptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SLAs&lt;/strong&gt; make late completion visible to operators.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency&lt;/strong&gt; ensures that rerunning or retrying a task produces the same correct result instead of duplicates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This tutorial uses a daily revenue aggregation pipeline as an example. It assumes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apache Airflow 2.x, where classic task-level SLA monitoring is available.&lt;/li&gt;
&lt;li&gt;PostgreSQL as the warehouse.&lt;/li&gt;
&lt;li&gt;A source table named &lt;code&gt;orders&lt;/code&gt; and a reporting table named &lt;code&gt;daily_order_revenue&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A daily DAG that processes a closed data interval.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Airflow 3 introduces newer alerting concepts such as Deadline Alerts, so verify the behavior of SLA-related settings against the version you operate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture: separate orchestration from data correctness
&lt;/h2&gt;

&lt;p&gt;Consider this pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL orders table
        |
        |  Read one Airflow data interval
        v
daily_revenue DAG task
        |
        |  Delete and rebuild one report partition
        v
PostgreSQL daily_order_revenue table
        |
        +--&amp;gt; Retry policy handles temporary failures
        +--&amp;gt; SLA callback alerts when completion is late
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The task processes one interval at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2025-03-10 00:00:00 UTC, 2025-03-11 00:00:00 UTC)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That interval is more important than the wall-clock time at which the task happens to run. A manual rerun next week should still process the same logical interval, not “today.”&lt;/p&gt;

&lt;p&gt;This leads to a useful division of responsibility:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Which interval should be processed?&lt;/td&gt;
&lt;td&gt;Airflow schedule and data interval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How should transient failure be handled?&lt;/td&gt;
&lt;td&gt;Retry configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What happens if the same interval runs twice?&lt;/td&gt;
&lt;td&gt;Idempotent warehouse write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do operators learn that output is late?&lt;/td&gt;
&lt;td&gt;SLA monitoring or deadline alerting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How is permanent bad input handled?&lt;/td&gt;
&lt;td&gt;Validation and explicit task failure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Retries are not a substitute for idempotency. A retry means the task may execute its write logic multiple times. If each execution appends rows without a deterministic key or replacement strategy, the retry itself can create incorrect output.&lt;/p&gt;




&lt;p&gt;Start your data engineering journey with &lt;a href="https://pavandeore.gumroad.com/l/data-engineering-complete-roadmap-for-beginners" rel="noopener noreferrer"&gt;Data Engineering: A Complete Roadmap for Beginners&lt;/a&gt; cuts through the noise and explains the essentials in a friendly, beginner-focused way across 15 comprehensive chapters and 190 pages.&lt;/p&gt;




&lt;h2&gt;
  
  
  Model retries around failure types
&lt;/h2&gt;

&lt;p&gt;Not every error deserves a retry.&lt;/p&gt;

&lt;p&gt;A retry is useful when the next attempt may reasonably succeed without changing the code or input:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Temporary network failure to the database&lt;/li&gt;
&lt;li&gt;Database deadlock or lock timeout&lt;/li&gt;
&lt;li&gt;A short-lived warehouse outage&lt;/li&gt;
&lt;li&gt;Rate limiting from an external API&lt;/li&gt;
&lt;li&gt;Object storage request timeout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A retry is usually wasteful for deterministic failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing required source column&lt;/li&gt;
&lt;li&gt;Invalid SQL syntax&lt;/li&gt;
&lt;li&gt;Bad credentials&lt;/li&gt;
&lt;li&gt;A malformed input file&lt;/li&gt;
&lt;li&gt;A violated business rule, such as negative revenue when negatives are forbidden&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Airflow retries are configured per task. In this example:&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;retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="n"&gt;retry_delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;retry_exponential_backoff&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;max_retry_delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means Airflow makes the initial attempt plus up to three retry attempts. Exponential backoff avoids having many failed tasks retry at the same moment after a shared outage.&lt;/p&gt;

&lt;p&gt;Also use &lt;code&gt;execution_timeout&lt;/code&gt;. A retry policy cannot help a task that hangs forever waiting on a database operation.&lt;/p&gt;

&lt;p&gt;A good starting point is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep retry counts modest.&lt;/li&gt;
&lt;li&gt;Use a short initial delay for likely transient failures.&lt;/li&gt;
&lt;li&gt;Cap exponential backoff.&lt;/li&gt;
&lt;li&gt;Set an execution timeout based on the expected task duration.&lt;/li&gt;
&lt;li&gt;Make writes safe to repeat before enabling retries.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Build an idempotent daily aggregation task
&lt;/h2&gt;

&lt;p&gt;The following DAG task rebuilds a single reporting partition. It uses Airflow’s &lt;code&gt;data_interval_start&lt;/code&gt; and &lt;code&gt;data_interval_end&lt;/code&gt; instead of &lt;code&gt;datetime.now()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The destination table has a primary key on &lt;code&gt;(report_date, order_id)&lt;/code&gt;, but the task also deletes the entire daily partition before inserting. That deletion handles a subtle case: an order that was present in a prior attempt but is no longer eligible in a later rerun.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example schema
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;paid_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount_cents&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;daily_order_revenue&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;report_date&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount_cents&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;loaded_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Airflow DAG
&lt;/h3&gt;

&lt;p&gt;Save this as &lt;code&gt;dags/daily_revenue.py&lt;/code&gt;. It requires a PostgreSQL Airflow connection named &lt;code&gt;warehouse_postgres&lt;/code&gt;.&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.decorators&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.operators.python&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_current_context&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.providers.postgres.hooks.postgres&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PostgresHook&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report_sla_miss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blocking_task_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blocking_tis&lt;/span&gt;&lt;span class="p"&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SLA missed for DAG=%s tasks=%s blocking_tasks=%s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;task_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;blocking_task_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@dag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily_revenue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0 0 * * *&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;catchup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_active_runs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sla_miss_callback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;report_sla_miss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&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;reporting&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;postgres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&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;daily_revenue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;retry_delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;retry_exponential_backoff&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_retry_delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;execution_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

        &lt;span class="c1"&gt;# See the SLA section for why this is 26 hours.
&lt;/span&gt;        &lt;span class="n"&gt;sla&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;),&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;rebuild_daily_revenue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_current_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;interval_start&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;data_interval_start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;interval_end&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;data_interval_end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;report_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;interval_start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        DELETE FROM daily_order_revenue
        WHERE report_date = %s;

        INSERT INTO daily_order_revenue (
            report_date,
            order_id,
            amount_cents
        )
        SELECT
            %s AS report_date,
            order_id,
            amount_cents
        FROM orders
        WHERE paid_at &amp;gt;= %s
          AND paid_at &amp;lt; %s
          AND status = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;paid&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="n"&gt;hook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PostgresHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgres_conn_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warehouse_postgres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Use one database transaction: either replacement succeeds fully,
&lt;/span&gt;        &lt;span class="c1"&gt;# or the previous partition remains unchanged after rollback.
&lt;/span&gt;        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_conn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;report_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;report_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;interval_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;interval_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nf"&gt;rebuild_daily_revenue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="nf"&gt;daily_revenue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suppose the DAG run processes &lt;code&gt;2025-03-10&lt;/code&gt; and the source contains:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;paid_at&lt;/th&gt;
&lt;th&gt;amount_cents&lt;/th&gt;
&lt;th&gt;status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;2025-03-10 08:15:00+00&lt;/td&gt;
&lt;td&gt;2599&lt;/td&gt;
&lt;td&gt;paid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;2025-03-10 11:40:00+00&lt;/td&gt;
&lt;td&gt;4999&lt;/td&gt;
&lt;td&gt;paid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;2025-03-10 15:00:00+00&lt;/td&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;td&gt;cancelled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;2025-03-11 00:00:00+00&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;td&gt;paid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The expected destination rows are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;report_date&lt;/th&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;amount_cents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2025-03-10&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;2599&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-03-10&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;4999&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Order &lt;code&gt;103&lt;/code&gt; is excluded because it was cancelled. Order &lt;code&gt;104&lt;/code&gt; is excluded because the interval is half-open: it starts exactly at the next day’s boundary.&lt;/p&gt;

&lt;p&gt;If the task fails after the &lt;code&gt;DELETE&lt;/code&gt; but before the &lt;code&gt;INSERT&lt;/code&gt;, the database transaction rolls back. If Airflow retries after a successful insert, the retry deletes and recreates the same partition. Both outcomes are safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand SLA timing before setting an SLA
&lt;/h2&gt;

&lt;p&gt;An SLA is an operational expectation, not a data-quality guarantee. In classic Airflow SLA behavior, a missed SLA can trigger a callback or notification, but it does not automatically stop a task, repair data, or create a retry.&lt;/p&gt;

&lt;p&gt;The confusing part is the reference time.&lt;/p&gt;

&lt;p&gt;For a daily schedule at midnight, a run that executes shortly after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2025-03-11 00:00 UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;typically processes this interval:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2025-03-10 00:00 UTC through 2025-03-11 00:00 UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its logical date is associated with the start of the interval: &lt;code&gt;2025-03-10 00:00 UTC&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the business requirement is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Yesterday’s revenue must be ready by 02:00 UTC on the next calendar day.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then the deadline is 26 hours after the interval start, not 2 hours after it. That is why the example uses:&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;sla&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;code&gt;sla=timedelta(hours=2)&lt;/code&gt; setting would be interpreted relative to the logical date and may already be overdue when the scheduled run begins.&lt;/p&gt;

&lt;p&gt;Classic SLA monitoring also depends on a healthy scheduler. Treat SLA alerts as a signal to investigate, not as the only mechanism for delivery monitoring.&lt;/p&gt;

&lt;p&gt;For a broader learning path that connects Airflow concepts with warehouse modeling and orchestration fundamentals, this optional &lt;a href="https://pavandeore.gumroad.com/l/data-engineering-complete-roadmap-for-beginners" rel="noopener noreferrer"&gt;data engineering roadmap&lt;/a&gt; can be a useful next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid the “idempotent SQL but changing source” trap
&lt;/h2&gt;

&lt;p&gt;The partition replacement pattern is idempotent with respect to a fixed source state. But source state can change.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The first attempt reads an order with &lt;code&gt;status = 'paid'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The task fails after committing the destination write, or is manually rerun later.&lt;/li&gt;
&lt;li&gt;Someone changes the source order to &lt;code&gt;status = 'refunded'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The rerun produces a different daily report.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That may be correct or incorrect depending on the business definition.&lt;/p&gt;

&lt;p&gt;Decide explicitly whether your report is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;As-of processing time&lt;/strong&gt;: reruns should use the latest source state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;As-of interval close&lt;/strong&gt;: reruns should reproduce the original result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eventually corrected&lt;/strong&gt;: late arrivals and corrections are expected, and recent partitions are intentionally rebuilt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For reproducible historical output, use immutable source files, source snapshots, change-data-capture offsets, or a versioned bronze layer. Airflow retries alone cannot freeze mutable source data.&lt;/p&gt;

&lt;p&gt;Also avoid using &lt;code&gt;NOW()&lt;/code&gt; inside transformation predicates. This query is unsafe for backfills:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;paid_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It produces a different answer each time it runs. Use the Airflow interval boundaries instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test retries, writes, and schedules separately
&lt;/h2&gt;

&lt;p&gt;Test the transformation SQL independently from Airflow. Insert representative rows around boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exactly at &lt;code&gt;data_interval_start&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;One microsecond before &lt;code&gt;data_interval_end&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Exactly at &lt;code&gt;data_interval_end&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Cancelled or refunded orders&lt;/li&gt;
&lt;li&gt;Duplicate source events, if your source allows them&lt;/li&gt;
&lt;li&gt;Late-arriving events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then verify these properties:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;One interval produces only expected rows.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Running the same interval twice produces the same destination state.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A failure between delete and insert leaves no partial partition.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adjacent intervals do not overlap or leave gaps.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A backfill uses historical interval boundaries, not the current date.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the transactional behavior, deliberately raise an exception after the &lt;code&gt;DELETE&lt;/code&gt; in a non-production test environment. Confirm that the transaction rolls back and the prior partition remains available.&lt;/p&gt;

&lt;p&gt;For DAG-level tests, Airflow can parse and validate the DAG without executing it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;airflow dags list | &lt;span class="nb"&gt;grep &lt;/span&gt;daily_revenue
airflow dags &lt;span class="nb"&gt;test &lt;/span&gt;daily_revenue 2025-03-11
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dags test&lt;/code&gt; command is useful for validating task execution for a logical date, but it is not a replacement for an integration test database. Use an isolated PostgreSQL schema or disposable database for tests that mutate tables.&lt;/p&gt;

&lt;p&gt;Finally, test SLA callbacks differently from task logic. SLA behavior is scheduler-driven and time-dependent, so unit test the callback function directly and validate actual alert routing in a staging Airflow environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production pitfalls to plan for
&lt;/h2&gt;

&lt;p&gt;A few configuration choices often cause surprising behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;catchup=False&lt;/code&gt; does not make backfills impossible.&lt;/strong&gt; Manual runs can still be created, so interval-based SQL remains necessary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;max_active_runs=1&lt;/code&gt; protects a mutable daily partition table.&lt;/strong&gt; Without it, overlapping runs for the same business date can race unless the write strategy is designed for concurrency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retries can overload a failing dependency.&lt;/strong&gt; Use exponential backoff and consider Airflow pools for shared databases or APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not put secrets in DAG code.&lt;/strong&gt; Use Airflow connections, a secrets backend, or workload identity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A task can succeed with bad data.&lt;/strong&gt; Add validation tasks for row counts, null rates, reconciliation totals, or expected freshness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SLA misses do not imply data loss.&lt;/strong&gt; A task may finish late but correctly; conversely, a task can finish on time with incorrect results.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Reliable Airflow DAGs treat retries, SLAs, and idempotency as one design problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retry only failures likely to be transient.&lt;/li&gt;
&lt;li&gt;Use data intervals, not current timestamps, to define work.&lt;/li&gt;
&lt;li&gt;Make destination writes safe to repeat with transactions and deterministic partition replacement or upserts.&lt;/li&gt;
&lt;li&gt;Set SLAs relative to Airflow’s logical date semantics.&lt;/li&gt;
&lt;li&gt;Test reruns, interval boundaries, rollback behavior, and mutable-source assumptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a task can be retried or manually rerun without changing the correctness of its output, Airflow becomes much safer to operate under real-world failure conditions.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>database</category>
      <category>dataengineering</category>
      <category>postgres</category>
    </item>
    <item>
      <title>CDC with Debezium and Kafka: A Source-to-Sink Walkthrough</title>
      <dc:creator>pawan deore</dc:creator>
      <pubDate>Tue, 03 Nov 2026 02:18:45 +0000</pubDate>
      <link>https://dev.to/pawandeore/cdc-with-debezium-and-kafka-a-source-to-sink-walkthrough-3o7e</link>
      <guid>https://dev.to/pawandeore/cdc-with-debezium-and-kafka-a-source-to-sink-walkthrough-3o7e</guid>
      <description>&lt;h1&gt;
  
  
  CDC with Debezium and Kafka: A Source-to-Sink Walkthrough
&lt;/h1&gt;

&lt;p&gt;Change Data Capture (CDC) moves database changes as events instead of repeatedly querying whole tables. In this walkthrough, PostgreSQL is the source of truth, Debezium reads its write-ahead log (WAL), Kafka transports the change events, and a small Python consumer maintains a SQLite projection as the sink.&lt;/p&gt;

&lt;p&gt;The example uses a &lt;code&gt;customers&lt;/code&gt; table, but the same pattern applies to operational databases feeding search indexes, warehouses, caches, downstream services, or lakehouse ingestion pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture and Delivery Model
&lt;/h2&gt;

&lt;p&gt;The pipeline has four responsibilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL
   │
   │ logical decoding from WAL
   ▼
Debezium PostgreSQL connector
   │
   │ Kafka Connect source task
   ▼
Kafka topic: shop.public.customers
   │
   │ ordered per Kafka partition
   ▼
Python sink consumer
   │
   ▼
SQLite customer_projection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL records every committed change in its WAL. Debezium does not poll &lt;code&gt;SELECT * FROM customers&lt;/code&gt;; instead, it creates or uses a logical replication slot and converts committed WAL changes into Kafka records.&lt;/p&gt;

&lt;p&gt;A typical lifecycle is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Debezium performs an initial snapshot of the configured table.&lt;/li&gt;
&lt;li&gt;It records the WAL position associated with that snapshot.&lt;/li&gt;
&lt;li&gt;It streams later inserts, updates, and deletes from the WAL.&lt;/li&gt;
&lt;li&gt;A sink consumes Kafka records and updates its own representation of the data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This architecture is usually &lt;strong&gt;at-least-once&lt;/strong&gt; end to end. A consumer can process the same event again after a restart, rebalance, or failure between writing to the sink and committing its Kafka offset. Therefore, sink writes must be idempotent.&lt;/p&gt;




&lt;p&gt;Start your data engineering journey with &lt;a href="https://pavandeore.gumroad.com/l/data-engineering-complete-roadmap-for-beginners" rel="noopener noreferrer"&gt;Data Engineering: A Complete Roadmap for Beginners&lt;/a&gt; cuts through the noise and explains the essentials in a friendly, beginner-focused way across 15 comprehensive chapters and 190 pages.&lt;/p&gt;




&lt;p&gt;For a table-backed sink, the usual idempotent pattern is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use the source primary key as the Kafka message key;&lt;/li&gt;
&lt;li&gt;upsert inserts and updates;&lt;/li&gt;
&lt;li&gt;delete by primary key;&lt;/li&gt;
&lt;li&gt;commit the Kafka offset only after the sink transaction commits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prepare PostgreSQL for Logical Replication
&lt;/h2&gt;

&lt;p&gt;Assume the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL is reachable at &lt;code&gt;localhost:5432&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Kafka is reachable at &lt;code&gt;localhost:9092&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Kafka Connect is running at &lt;code&gt;http://localhost:8083&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the Kafka Connect worker has the Debezium PostgreSQL connector plugin installed;&lt;/li&gt;
&lt;li&gt;PostgreSQL has &lt;code&gt;wal_level=logical&lt;/code&gt; and has been restarted after that setting was changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the following as a PostgreSQL administrator. The &lt;code&gt;ALTER SYSTEM&lt;/code&gt; command requires a database restart before it takes effect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;SYSTEM&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;wal_level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'logical'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;debezium&lt;/span&gt;
  &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt; &lt;span class="n"&gt;REPLICATION&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'dbz_password'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;appdb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connect to &lt;code&gt;appdb&lt;/code&gt;, then create a source table and initial rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;         &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;full_name&lt;/span&gt;  &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Aisha Rahman'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'aisha@example.com'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Mark Chen'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'mark@example.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;debezium&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;debezium&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;PUBLICATION&lt;/span&gt; &lt;span class="n"&gt;dbz_publication&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The publication tells PostgreSQL which table changes may be emitted through the &lt;code&gt;pgoutput&lt;/code&gt; logical decoding plugin.&lt;/p&gt;

&lt;p&gt;The primary key is important. Debezium uses it to construct a stable Kafka key such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without a stable key, a downstream sink cannot reliably identify which row should be updated or deleted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Register the Debezium Connector
&lt;/h2&gt;

&lt;p&gt;Kafka Connect runs connectors. Debezium is a &lt;strong&gt;source connector&lt;/strong&gt;: it reads from PostgreSQL and writes to Kafka.&lt;/p&gt;

&lt;p&gt;Save the following as &lt;code&gt;customers-connector.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customers-cdc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"connector.class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"io.debezium.connector.postgresql.PostgresConnector"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"plugin.name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pgoutput"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"database.hostname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"database.port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5432"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"database.user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"debezium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"database.password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dbz_password"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"database.dbname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"appdb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"topic.prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"table.include.list"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public.customers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"publication.name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dbz_publication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"publication.autocreate.mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"disabled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"slot.name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customers_cdc_slot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"snapshot.mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"initial"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"schema.history.internal.kafka.bootstrap.servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"localhost:9092"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schema.history.internal.kafka.topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dbz-schema-history.shop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"key.converter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"org.apache.kafka.connect.json.JsonConverter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"key.converter.schemas.enable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value.converter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"org.apache.kafka.connect.json.JsonConverter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value.converter.schemas.enable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"transforms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unwrap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"transforms.unwrap.type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"io.debezium.transforms.ExtractNewRecordState"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"transforms.unwrap.add.fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"op,source.ts_ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"transforms.unwrap.delete.handling.mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rewrite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"transforms.unwrap.delete.tombstone.handling.mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"drop"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register it with Kafka Connect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; @customers-connector.json &lt;span class="se"&gt;\&lt;/span&gt;
  http://localhost:8083/connectors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check connector and task status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8083/connectors/customers-cdc/status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source topic is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shop.public.customers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the &lt;code&gt;ExtractNewRecordState&lt;/code&gt; transformation, Debezium emits a detailed envelope containing &lt;code&gt;before&lt;/code&gt;, &lt;code&gt;after&lt;/code&gt;, source metadata, transaction context, and operation type. That full envelope is valuable for audit pipelines.&lt;/p&gt;

&lt;p&gt;This tutorial unwraps the &lt;code&gt;after&lt;/code&gt; state because the sink is maintaining a current-state table. It adds metadata fields such as &lt;code&gt;__op&lt;/code&gt;, where values commonly include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;r&lt;/code&gt;: snapshot read&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;c&lt;/code&gt;: create&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;u&lt;/code&gt;: update&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;d&lt;/code&gt;: delete&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a broader structured learning path after building this example, &lt;a href="https://pavandeore.gumroad.com/l/data-engineering-complete-roadmap-for-beginners" rel="noopener noreferrer"&gt;this data engineering roadmap&lt;/a&gt; is an optional next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate Changes and Inspect the Kafka Records
&lt;/h2&gt;

&lt;p&gt;Make a few source-side changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'aisha.rahman@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Omar Haddad'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'omar@example.com'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After unwrapping, the update event is approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"full_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Aisha Rahman"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aisha.rahman@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"updated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"__op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"u"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"__source_ts_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1730000000000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact timestamps will differ. A delete is rewritten into a value containing a deletion marker, typically &lt;code&gt;__deleted: "true"&lt;/code&gt;, while retaining the Kafka key.&lt;/p&gt;

&lt;p&gt;You can inspect records using Kafka CLI tools. The exact command depends on the Kafka distribution, but the essential requirement is to display both key and value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kafka-console-consumer &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; localhost:9092 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic&lt;/span&gt; shop.public.customers &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-beginning&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--property&lt;/span&gt; print.key&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--property&lt;/span&gt; key.separator&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;" | "&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Build a Small Idempotent Python Sink
&lt;/h2&gt;

&lt;p&gt;This consumer writes the topic into SQLite. It is intentionally small, but it demonstrates the core production rule: write to the sink first, then commit the Kafka offset.&lt;/p&gt;

&lt;p&gt;Install the dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;kafka-python
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save this as &lt;code&gt;customer_sink.py&lt;/code&gt;:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kafka&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KafkaConsumer&lt;/span&gt;

&lt;span class="n"&gt;TOPIC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shop.public.customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&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_projection.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
CREATE TABLE IF NOT EXISTS customer_projection (
    id INTEGER PRIMARY KEY,
    full_name TEXT NOT NULL,
    email TEXT NOT NULL,
    updated_at TEXT NOT NULL
)
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;consumer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KafkaConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TOPIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bootstrap_servers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:9092&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;group_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlite-customer-sink-v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;auto_offset_reset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;earliest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;enable_auto_commit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;key_deserializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&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;raw&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;value_deserializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&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;raw&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&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;key&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;key&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;Missing primary-key message key: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&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;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;is_delete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__deleted&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="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;lower&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;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;db&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;is_delete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE FROM customer_projection WHERE id = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
            &lt;span class="p"&gt;)&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                INSERT INTO customer_projection (id, full_name, email, updated_at)
                VALUES (?, ?, ?, ?)
                ON CONFLICT(id) DO UPDATE SET
                    full_name = excluded.full_name,
                    email = excluded.email,
                    updated_at = excluded.updated_at
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# Offset is committed only after SQLite commits successfully.
&lt;/span&gt;    &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;If the consumer reads the initial snapshot followed by the three changes above, query the sink database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sqlite3 customer_projection.db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"SELECT id, full_name, email FROM customer_projection ORDER BY id;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1|Aisha Rahman|aisha.rahman@example.com
3|Omar Haddad|omar@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;id=2&lt;/code&gt; is absent because its source row was deleted.&lt;/p&gt;

&lt;p&gt;The upsert makes repeated create, snapshot, and update events safe. The delete is also idempotent: deleting an already absent row is harmless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle Ordering, Duplicates, and Schema Changes Deliberately
&lt;/h2&gt;

&lt;p&gt;Kafka guarantees order only &lt;strong&gt;within one partition&lt;/strong&gt;. Records with the same non-null key are normally routed to the same partition, so all changes for customer &lt;code&gt;id=1&lt;/code&gt; retain order.&lt;/p&gt;

&lt;p&gt;Do not assume a global order across customers. An update to customer &lt;code&gt;1&lt;/code&gt; and an insert for customer &lt;code&gt;3&lt;/code&gt; may be processed in different partitions and in a different relative order than another system observes them.&lt;/p&gt;

&lt;p&gt;Duplicates are normal under at-least-once delivery. The SQLite sample handles them because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inserts are upserts;&lt;/li&gt;
&lt;li&gt;updates overwrite the same primary key;&lt;/li&gt;
&lt;li&gt;deletes are repeatable;&lt;/li&gt;
&lt;li&gt;the consumer commits offsets after the sink write.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Schema evolution needs an explicit plan. Adding a nullable source column is usually straightforward, but dropping or renaming columns can break consumers. For production pipelines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;version event contracts;&lt;/li&gt;
&lt;li&gt;deploy sink support before emitting a required new field;&lt;/li&gt;
&lt;li&gt;avoid &lt;code&gt;SELECT *&lt;/code&gt;-style assumptions in consumers;&lt;/li&gt;
&lt;li&gt;use a schema registry and compatibility rules if multiple services consume the topic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The JSON converter simplifies this tutorial, but it does not provide strong schema governance. Avro, Protobuf, or JSON Schema with a registry is often preferable for shared event streams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Pitfalls and Operational Controls
&lt;/h2&gt;

&lt;p&gt;A replication slot retains WAL until Debezium confirms it has consumed it. If Kafka Connect is down for too long, PostgreSQL disk usage can grow because old WAL segments cannot be removed. Monitor replication-slot lag and WAL disk usage.&lt;/p&gt;

&lt;p&gt;Other common pitfalls include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No primary key:&lt;/strong&gt; deletes and updates become difficult to apply safely downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot surprises:&lt;/strong&gt; an &lt;code&gt;initial&lt;/code&gt; snapshot can produce many records. Ensure the sink can handle the initial volume and that topic retention is sufficient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connector restarts:&lt;/strong&gt; source offsets are stored by Kafka Connect. Losing Connect internal topics can cause unwanted re-snapshots or duplicate processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poison messages:&lt;/strong&gt; one malformed record can repeatedly fail a consumer. Production consumers should validate records, log context, and use a dead-letter strategy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete semantics:&lt;/strong&gt; decide whether sinks need hard deletes, soft deletes, tombstones, or historical retention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database permissions:&lt;/strong&gt; grant Debezium only the replication and read permissions it needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Topic management:&lt;/strong&gt; pre-create production topics with intentional partition counts, retention, compaction choices, ACLs, and monitoring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a current-state projection, consider log compaction on the CDC topic only after understanding delete tombstones and consumer replay requirements. A compacted topic is useful for rebuilding latest state, while a retained append-only topic is more useful for historical replay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the Pipeline and Verify Recovery Behavior
&lt;/h2&gt;

&lt;p&gt;Test CDC as a data contract, not just a connectivity check.&lt;/p&gt;

&lt;p&gt;Start with an integration test environment containing PostgreSQL, Kafka, Kafka Connect, and a disposable sink. Then verify:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initial snapshot:&lt;/strong&gt; source rows appear in the sink.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insert:&lt;/strong&gt; a new source row appears once logically in the sink.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update:&lt;/strong&gt; changed values replace old values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete:&lt;/strong&gt; the sink removes or marks the row according to your contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restart recovery:&lt;/strong&gt; stop the sink after a database write but before an offset commit, then restart it. The duplicate event must not corrupt the result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connector recovery:&lt;/strong&gt; restart Kafka Connect and confirm streaming resumes from stored offsets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema change:&lt;/strong&gt; add a nullable column and verify consumers tolerate it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A useful assertion compares source and sink after all events have settled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that result with the sink projection query. For this current-state sink, they should match on the columns intentionally replicated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Debezium CDC turns committed PostgreSQL changes into Kafka events by reading the WAL, not by polling tables. A reliable source-to-sink pipeline depends on stable primary keys, idempotent sink writes, offset commits after durable writes, and monitoring of replication-slot lag.&lt;/p&gt;

&lt;p&gt;The smallest useful mental model is: PostgreSQL commits a change, Debezium publishes it, Kafka retains it, and the sink repeatedly applies it safely until its state converges with the source.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>database</category>
      <category>python</category>
      <category>streaming</category>
    </item>
    <item>
      <title>Build an Incremental ETL Pipeline in Python Using PostgreSQL Watermarks</title>
      <dc:creator>pawan deore</dc:creator>
      <pubDate>Mon, 02 Nov 2026 17:20:50 +0000</pubDate>
      <link>https://dev.to/pawandeore/build-an-incremental-etl-pipeline-in-python-using-postgresql-watermarks-12h5</link>
      <guid>https://dev.to/pawandeore/build-an-incremental-etl-pipeline-in-python-using-postgresql-watermarks-12h5</guid>
      <description>&lt;h1&gt;
  
  
  Build an Incremental ETL Pipeline in Python Using PostgreSQL Watermarks
&lt;/h1&gt;

&lt;p&gt;Incremental ETL avoids rereading an entire source table on every run. Instead, the pipeline remembers the latest source change it successfully loaded, then requests only records after that point. That remembered position is the &lt;strong&gt;watermark&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This tutorial builds a small but production-oriented pipeline that copies &lt;code&gt;app.orders&lt;/code&gt; into &lt;code&gt;warehouse.fact_orders&lt;/code&gt;. It uses PostgreSQL timestamps plus a primary-key tie-breaker as the watermark and performs idempotent upserts in Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the Watermark Contract First
&lt;/h2&gt;

&lt;p&gt;A watermark is only reliable when its source column has a clear contract. In this example, each source row has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;updated_at&lt;/code&gt;: when the application last changed the row&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt;: a stable, unique primary key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The checkpoint is the pair:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(last_updated_at, last_id)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rows are ordered lexicographically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(updated_at, id)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, if the saved watermark is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(2025-03-08 10:00:00+00, 42)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the next extraction includes rows where:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-03-08 10:00:00+00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;id&lt;/code&gt; is important. Multiple rows can receive the same timestamp, especially when inserts happen in a batch. Without a tie-breaker, rows sharing the previous maximum timestamp may be skipped.&lt;/p&gt;

&lt;p&gt;This design assumes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;updated_at&lt;/code&gt; is populated by the database or application for every insert and update.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;updated_at&lt;/code&gt; is never intentionally moved backward.&lt;/li&gt;
&lt;li&gt;Source rows are not physically deleted without another way to represent deletion.&lt;/li&gt;
&lt;li&gt;The pipeline can safely reload a small overlap window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The final assumption matters because timestamp watermarks are not a full change data capture system. We will revisit that limitation later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create Source, Target, and Pipeline-State Tables
&lt;/h2&gt;

&lt;p&gt;For a small local example, use one PostgreSQL database with separate schemas. In a real system, the source and warehouse may be different databases, clusters, or services.&lt;/p&gt;

&lt;p&gt;Run this SQL first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_cents&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_cents&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;updated_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;clock_timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Ensure application updates refresh the watermark column.&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_updated_at&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clock_timestamp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;orders_set_updated_at&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;orders_set_updated_at&lt;/span&gt;
&lt;span class="k"&gt;BEFORE&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;
&lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_updated_at&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fact_orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_cents&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;source_updated_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;loaded_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;clock_timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pipeline_state&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;pipeline_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_updated_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;clock_timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2599&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;102&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4999&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;103&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The target table retains &lt;code&gt;source_updated_at&lt;/code&gt;. That field is useful both for observability and for preventing an older reprocessed record from overwriting a newer target version.&lt;/p&gt;




&lt;p&gt;Start your data engineering journey with &lt;a href="https://pavandeore.gumroad.com/l/data-engineering-complete-roadmap-for-beginners" rel="noopener noreferrer"&gt;Data Engineering: A Complete Roadmap for Beginners&lt;/a&gt; cuts through the noise and explains the essentials in a friendly, beginner-focused way across 15 comprehensive chapters and 190 pages.&lt;/p&gt;




&lt;h2&gt;
  
  
  Understand the Pipeline Architecture and Transaction Boundaries
&lt;/h2&gt;

&lt;p&gt;The pipeline has four logical components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL source table
        |
        |  rows after saved watermark
        v
Python extraction and transformation
        |
        |  idempotent upsert
        v
PostgreSQL warehouse table
        |
        |  successful load only
        v
PostgreSQL pipeline_state checkpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical correctness rule is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Advance the watermark only after the target rows have been written successfully.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the process fails during loading, the saved checkpoint must not move. On the next execution, the same source rows are read again. That requires target writes to be &lt;strong&gt;idempotent&lt;/strong&gt;: loading the same source row twice must produce the same final target state.&lt;/p&gt;

&lt;p&gt;This example uses a transaction around target upserts and the state update. It also obtains a PostgreSQL advisory lock so two scheduler invocations cannot run the same pipeline concurrently.&lt;/p&gt;

&lt;p&gt;The source extraction uses a &lt;code&gt;REPEATABLE READ&lt;/code&gt; transaction. That gives the run a consistent source snapshot: the upper watermark and extracted rows describe the same view of the source table.&lt;/p&gt;

&lt;p&gt;If you want a broader structured learning path after implementing this pattern, &lt;a href="https://pavandeore.gumroad.com/l/data-engineering-complete-roadmap-for-beginners" rel="noopener noreferrer"&gt;this data engineering roadmap&lt;/a&gt; is an optional next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the Incremental Python ETL Job
&lt;/h2&gt;

&lt;p&gt;Install the PostgreSQL driver:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"psycopg[binary]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set a connection string. This example uses the same database for source and target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"postgresql://postgres:postgres@localhost:5432/analytics"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the following as &lt;code&gt;incremental_orders.py&lt;/code&gt;:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;psycopg.rows&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dict_row&lt;/span&gt;

&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;PIPELINE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders_incremental_v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Re-read recent changes to tolerate slightly late commits.
# Target upserts make this safe.
&lt;/span&gt;&lt;span class="n"&gt;LOOKBACK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;INITIAL_WATERMARK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1970&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&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;run_pipeline&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;with &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dict_row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;source_conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dict_row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;target_conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# This transaction contains both target writes and state advancement.
&lt;/span&gt;        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;target_conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;target_conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;target_cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="c1"&gt;# Prevent concurrent runs of this exact pipeline.
&lt;/span&gt;                &lt;span class="n"&gt;target_cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT pg_advisory_xact_lock(hashtext(%s))&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PIPELINE_NAME&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="n"&gt;target_cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                    SELECT last_updated_at, last_id
                    FROM warehouse.pipeline_state
                    WHERE pipeline_name = %s
                    FOR UPDATE
                    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PIPELINE_NAME&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;saved_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target_cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&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;saved_state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="c1"&gt;# Intentionally overlap prior work. id=0 means all IDs at
&lt;/span&gt;                    &lt;span class="c1"&gt;# the overlap timestamp range are eligible for reload.
&lt;/span&gt;                    &lt;span class="n"&gt;lower_updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;saved_state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_updated_at&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="n"&gt;LOOKBACK&lt;/span&gt;
                    &lt;span class="n"&gt;lower_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;lower_updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;INITIAL_WATERMARK&lt;/span&gt;
                    &lt;span class="n"&gt;lower_id&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;# Read a consistent source snapshot.
&lt;/span&gt;                &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;source_conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;source_conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;source_cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;source_cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                        &lt;span class="p"&gt;)&lt;/span&gt;

                        &lt;span class="c1"&gt;# Capture an upper bound from the same source snapshot.
&lt;/span&gt;                        &lt;span class="n"&gt;source_cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                            SELECT updated_at, id
                            FROM app.orders
                            ORDER BY updated_at DESC, id DESC
                            LIMIT 1
                            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
                        &lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="n"&gt;upper_bound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source_cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&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;upper_bound&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Source table is empty; nothing to load.&lt;/span&gt;&lt;span class="sh"&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;upper_updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;upper_bound&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                        &lt;span class="n"&gt;upper_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;upper_bound&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

                        &lt;span class="n"&gt;source_cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                            SELECT
                                id,
                                customer_id,
                                status,
                                total_cents,
                                updated_at AS source_updated_at
                            FROM app.orders
                            WHERE (updated_at, id) &amp;gt; (%s, %s)
                              AND (updated_at, id) &amp;lt;= (%s, %s)
                            ORDER BY updated_at, id
                            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="p"&gt;(&lt;/span&gt;
                                &lt;span class="n"&gt;lower_updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;lower_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;upper_updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;upper_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source_cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

                &lt;span class="c1"&gt;# Upsert makes retries and overlap reads safe.
&lt;/span&gt;                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;target_cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executemany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                        INSERT INTO warehouse.fact_orders (
                            order_id,
                            customer_id,
                            status,
                            total_cents,
                            source_updated_at
                        )
                        VALUES (
                            %(id)s,
                            %(customer_id)s,
                            %(status)s,
                            %(total_cents)s,
                            %(source_updated_at)s
                        )
                        ON CONFLICT (order_id) DO UPDATE
                        SET
                            customer_id = EXCLUDED.customer_id,
                            status = EXCLUDED.status,
                            total_cents = EXCLUDED.total_cents,
                            source_updated_at = EXCLUDED.source_updated_at,
                            loaded_at = clock_timestamp()
                        WHERE warehouse.fact_orders.source_updated_at
                              &amp;lt;= EXCLUDED.source_updated_at
                        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="c1"&gt;# Advance state only after all target upserts succeeded.
&lt;/span&gt;                &lt;span class="n"&gt;target_cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                    INSERT INTO warehouse.pipeline_state (
                        pipeline_name,
                        last_updated_at,
                        last_id
                    )
                    VALUES (%s, %s, %s)
                    ON CONFLICT (pipeline_name) DO UPDATE
                    SET
                        last_updated_at = EXCLUDED.last_updated_at,
                        last_id = EXCLUDED.last_id,
                        updated_at = clock_timestamp()
                    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PIPELINE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upper_updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upper_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="nf"&gt;print&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;Loaded or reprocessed &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rows. &lt;/span&gt;&lt;span class="sh"&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;New watermark: (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;upper_updated_at&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;upper_id&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;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;run_pipeline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verify Expected Results and Incremental Behavior
&lt;/h2&gt;

&lt;p&gt;After the first run, query the target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cents&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fact_orders&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;order_id&lt;/th&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;status&lt;/th&gt;
&lt;th&gt;total_cents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;paid&lt;/td&gt;
&lt;td&gt;2599&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;pending&lt;/td&gt;
&lt;td&gt;4999&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;paid&lt;/td&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The state table should contain one checkpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pipeline_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pipeline_state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now simulate an update and a new order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;104&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the Python job again. The pipeline will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reread a small 10-minute overlap,&lt;/li&gt;
&lt;li&gt;update order &lt;code&gt;2&lt;/code&gt; from &lt;code&gt;pending&lt;/code&gt; to &lt;code&gt;paid&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;insert the new order,&lt;/li&gt;
&lt;li&gt;advance the state to the newest &lt;code&gt;(updated_at, id)&lt;/code&gt; pair.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the load uses &lt;code&gt;INSERT ... ON CONFLICT DO UPDATE&lt;/code&gt;, overlap rows do not create duplicates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Pitfalls That Watermarks Do Not Solve Automatically
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Late commits can be missed without overlap.&lt;/strong&gt; An application transaction may assign &lt;code&gt;updated_at&lt;/code&gt;, remain open for several minutes, and commit after the ETL has already moved beyond that timestamp. The overlap window reduces this risk, but it is not a mathematical guarantee for arbitrarily long transactions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backdated timestamps break the contract.&lt;/strong&gt; If an application sets &lt;code&gt;updated_at&lt;/code&gt; from client-provided data or restores old timestamps during a migration, the row may fall behind the checkpoint and never be extracted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hard deletes are invisible.&lt;/strong&gt; A query for changed rows cannot detect a row that no longer exists. Use soft deletes such as &lt;code&gt;deleted_at&lt;/code&gt;, maintain a deletion tombstone table, or adopt change data capture (CDC).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timestamp precision matters.&lt;/strong&gt; PostgreSQL &lt;code&gt;TIMESTAMPTZ&lt;/code&gt; supports microseconds, but a timestamp alone is still not a unique event identifier. The &lt;code&gt;(updated_at, id)&lt;/code&gt; pair handles different rows with identical timestamps; it does not represent multiple distinct updates to the same row with the exact same timestamp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Large batches need staging.&lt;/strong&gt; &lt;code&gt;executemany&lt;/code&gt; is appropriate for a small tutorial. For high volume, load into a staging table with PostgreSQL &lt;code&gt;COPY&lt;/code&gt;, then perform a set-based &lt;code&gt;INSERT ... SELECT ... ON CONFLICT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For stronger guarantees across long-running transactions, deletes, and every row-level change, use PostgreSQL logical replication/CDC or an application outbox rather than relying solely on an &lt;code&gt;updated_at&lt;/code&gt; watermark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the Pipeline as a State Machine
&lt;/h2&gt;

&lt;p&gt;Test more than whether rows appear in the target. The state transition is the important behavior.&lt;/p&gt;

&lt;p&gt;Useful integration tests include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initial load:&lt;/strong&gt; insert source rows, run the job, and assert target row count and state values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotent retry:&lt;/strong&gt; run the job twice without source changes; assert no duplicate target rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update propagation:&lt;/strong&gt; update a source order, rerun, and assert the target reflects the new values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same-timestamp ordering:&lt;/strong&gt; insert multiple rows with a controlled identical &lt;code&gt;updated_at&lt;/code&gt;; confirm all are loaded because &lt;code&gt;id&lt;/code&gt; breaks ties.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure before checkpoint:&lt;/strong&gt; intentionally fail after extraction but before the state update, then rerun. Target data may be reprocessed, but no source change should be lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrent execution:&lt;/strong&gt; start two jobs and confirm the advisory lock serializes them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep test data small and inspect &lt;code&gt;warehouse.pipeline_state&lt;/code&gt; directly. A pipeline can look correct in the destination while silently holding an incorrect checkpoint that causes future data loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A PostgreSQL watermark pipeline is built on a simple rule: extract rows after a durable checkpoint, load them idempotently, and update that checkpoint only after the load succeeds. Use a composite &lt;code&gt;(updated_at, id)&lt;/code&gt; watermark, a bounded source snapshot, transactional target writes, and an overlap window. It is a practical pattern for mutable tables, but for strict handling of deletes and long-running transactions, move to CDC or an outbox-based design.&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Build an MCP Server with FastMCP and Connect It to Claude</title>
      <dc:creator>pawan deore</dc:creator>
      <pubDate>Mon, 17 Aug 2026 09:03:45 +0000</pubDate>
      <link>https://dev.to/pawandeore/build-an-mcp-server-with-fastmcp-and-connect-it-to-claude-3i5n</link>
      <guid>https://dev.to/pawandeore/build-an-mcp-server-with-fastmcp-and-connect-it-to-claude-3i5n</guid>
      <description>&lt;p&gt;Model Context Protocol (MCP) makes it possible for AI assistants to interact with external tools and data sources in a standardized way.&lt;/p&gt;

&lt;p&gt;In this tutorial, we'll build a simple &lt;strong&gt;MCP server using FastMCP&lt;/strong&gt;, expose it over HTTPS with LocalTunnel, and connect it to Claude as a custom connector.&lt;/p&gt;

&lt;p&gt;Our example will be a simple note-taking server with two tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;get_notes&lt;/code&gt; — retrieve saved notes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;add_note&lt;/code&gt; — add a new note&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end, Claude will be able to read from and write to our MCP server.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎥 Watch the Tutorial
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/O9_WGf1GZ5o" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What We'll Build
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude
   │
   │ MCP
   ▼
HTTPS Tunnel
   │
   ▼
FastMCP Server
   │
   ▼
Mock Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll use an in-memory list as our database for simplicity. In a real application, you could replace it with PostgreSQL, SQLite, MongoDB, or another database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3&lt;/li&gt;
&lt;li&gt;VS Code&lt;/li&gt;
&lt;li&gt;Node.js and npm&lt;/li&gt;
&lt;li&gt;A Claude account that supports custom connectors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create a new project and open it in VS Code.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Create a Virtual Environment
&lt;/h2&gt;

&lt;p&gt;Start by creating a Python virtual environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Activate it on macOS/Linux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Install FastMCP
&lt;/h2&gt;

&lt;p&gt;Install FastMCP using pip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;fastmcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FastMCP makes it much easier to create MCP servers in Python without implementing the MCP protocol yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Create the MCP Server
&lt;/h2&gt;

&lt;p&gt;Create a file named &lt;code&gt;main.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Start by importing FastMCP and creating an MCP server:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;

&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Simple Note Taker&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;For this tutorial, we'll use a simple Python list as our temporary database:&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;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&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;Remember to deploy server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&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;Eat Apple at 5 PM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We now have two initial notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Create the &lt;code&gt;get_notes&lt;/code&gt; Tool
&lt;/h2&gt;

&lt;p&gt;FastMCP allows us to turn a normal Python function into an MCP tool using the &lt;code&gt;@mcp.tool&lt;/code&gt; decorator.&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="nd"&gt;@mcp.tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_notes&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="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;output&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;Note #&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This function loops through our notes and returns them as a string.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Create the &lt;code&gt;add_note&lt;/code&gt; Tool
&lt;/h2&gt;

&lt;p&gt;Next, let's create a tool that allows Claude to add a new note.&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="nd"&gt;@mcp.tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_note&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&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="n"&gt;new_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;return&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;Successfully saved note #&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;new_id&lt;/span&gt;&lt;span class="si"&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 function accepts the note text, generates a new ID, stores the note, and returns a success message.&lt;/p&gt;

&lt;p&gt;Our MCP server now exposes two tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_notes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns all saved notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;add_note&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Adds a new note&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  6. Configure the MCP Server
&lt;/h2&gt;

&lt;p&gt;We want our MCP server to run over HTTP on port &lt;code&gt;8000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Add this to the bottom of &lt;code&gt;main.py&lt;/code&gt;:&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The complete &lt;code&gt;main.py&lt;/code&gt; looks like this:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;

&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Simple Note Taker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&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;Remember to deploy server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&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;Eat Apple at 5 PM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_notes&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="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;output&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;Note #&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;


&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_note&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&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="n"&gt;new_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;return&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;Successfully saved note #&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;new_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Run the MCP Server
&lt;/h2&gt;

&lt;p&gt;Start the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FastMCP should start listening on port &lt;code&gt;8000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Our MCP server is now running locally.&lt;/p&gt;

&lt;p&gt;However, Claude cannot directly access &lt;code&gt;localhost:8000&lt;/code&gt;, so we need to expose it through an HTTPS URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Create an HTTPS Tunnel with LocalTunnel
&lt;/h2&gt;

&lt;p&gt;For this tutorial, we'll use LocalTunnel.&lt;/p&gt;

&lt;p&gt;Install it globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; localtunnel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create a tunnel to port &lt;code&gt;8000&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lt &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LocalTunnel will provide an HTTPS URL similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://your-generated-url.loca.lt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the LocalTunnel process running while you're using the MCP server.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; LocalTunnel is useful for development and tutorials. For production, use a properly hosted HTTPS endpoint with authentication and appropriate security controls.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  9. Connect the MCP Server to Claude
&lt;/h2&gt;

&lt;p&gt;Now let's connect our MCP server to Claude.&lt;/p&gt;

&lt;p&gt;In Claude, open:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customize → Connectors&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then select:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add a custom connector&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Give the connector a name, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Custom Todo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the remote server URL, enter your LocalTunnel URL with the MCP endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://your-generated-url.loca.lt/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then click &lt;strong&gt;Add&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Claude will try to connect to your MCP server.&lt;/p&gt;

&lt;p&gt;If the connection succeeds, Claude should discover the two tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;add_note
get_notes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Approve the tools so Claude can use them.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Test the &lt;code&gt;get_notes&lt;/code&gt; Tool
&lt;/h2&gt;

&lt;p&gt;Go back to the Claude chat and ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Show me my notes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude should call the &lt;code&gt;get_notes&lt;/code&gt; MCP tool.&lt;/p&gt;

&lt;p&gt;You should get a response similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Note #0: Remember to deploy server
Note #1: Eat Apple at 5 PM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is that Claude is actually calling our MCP server to retrieve this information.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Test the &lt;code&gt;add_note&lt;/code&gt; Tool
&lt;/h2&gt;

&lt;p&gt;Now let's add a new note.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add a new note: Research MCP authentication.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude should recognize that &lt;code&gt;add_note&lt;/code&gt; is the appropriate MCP tool and call it with the provided text.&lt;/p&gt;

&lt;p&gt;The server will add the note to our in-memory database.&lt;/p&gt;

&lt;p&gt;Now ask Claude:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Show me my notes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should now see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Note #0: Remember to deploy server
Note #1: Eat Apple at 5 PM
Note #2: Research MCP authentication.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We've successfully created an MCP server and connected it to Claude.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Request Flows
&lt;/h2&gt;

&lt;p&gt;Here's what happens when you ask Claude to add a note:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You
 │
 │ "Add a new note..."
 ▼
Claude
 │
 │ MCP tool call
 ▼
HTTPS LocalTunnel URL
 │
 ▼
FastMCP Server
 │
 │ add_note()
 ▼
Mock Database
 │
 │ result
 ▼
Claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the core idea behind MCP: an AI assistant can discover and interact with external tools through a standardized protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Mock Database to a Real Database
&lt;/h2&gt;

&lt;p&gt;Our example uses a Python list:&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;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is fine for learning, but the data will disappear when the server restarts.&lt;/p&gt;

&lt;p&gt;A real application could use a persistent database such as PostgreSQL or SQLite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FastMCP Server
      │
      ├── get_notes()
      │       │
      │       ▼
      │    Database
      │
      └── add_note()
              │
              ▼
           Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could also add more tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;get_notes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add_note&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;update_note&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delete_note&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;search_notes&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, you have the foundation for a proper AI-powered task or note management system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Considerations
&lt;/h2&gt;

&lt;p&gt;The LocalTunnel setup is great for experimentation, but you should take additional precautions before deploying an MCP server to production.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using a stable HTTPS domain&lt;/li&gt;
&lt;li&gt;Adding authentication and authorization&lt;/li&gt;
&lt;li&gt;Validating tool inputs&lt;/li&gt;
&lt;li&gt;Using a persistent database&lt;/li&gt;
&lt;li&gt;Adding logging and monitoring&lt;/li&gt;
&lt;li&gt;Handling errors properly&lt;/li&gt;
&lt;li&gt;Limiting access to sensitive tools&lt;/li&gt;
&lt;li&gt;Protecting sensitive data&lt;/li&gt;
&lt;li&gt;Adding rate limiting where appropriate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is particularly important when an AI assistant has permission to perform actions on your behalf.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we built a simple MCP server with FastMCP and connected it to Claude.&lt;/p&gt;

&lt;p&gt;The process was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a Python virtual environment.&lt;/li&gt;
&lt;li&gt;Install FastMCP.&lt;/li&gt;
&lt;li&gt;Create an MCP server.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;get_notes&lt;/code&gt; and &lt;code&gt;add_note&lt;/code&gt; tools.&lt;/li&gt;
&lt;li&gt;Run the server on port &lt;code&gt;8000&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Expose the local server using LocalTunnel.&lt;/li&gt;
&lt;li&gt;Add the server as a custom connector in Claude.&lt;/li&gt;
&lt;li&gt;Use Claude to read and create notes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The example is intentionally simple, but the same pattern can be used to expose APIs, databases, internal services, automation workflows, and other capabilities to AI assistants.&lt;/p&gt;

&lt;p&gt;That's what makes MCP powerful: &lt;strong&gt;you can give an AI model access to capabilities, not just information.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're experimenting with MCP, start small. Build one useful tool, connect it to an MCP client, and then expand the server as your use case grows.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>claude</category>
      <category>fastmcp</category>
    </item>
    <item>
      <title> Day 2: Data Engineering vs Data Science vs Data Analytics</title>
      <dc:creator>pawan deore</dc:creator>
      <pubDate>Fri, 12 Dec 2025 11:51:43 +0000</pubDate>
      <link>https://dev.to/pawandeore/30-days-of-data-engineering-challenge-day-2-data-engineering-vs-data-science-vs-data-analytics-29l4</link>
      <guid>https://dev.to/pawandeore/30-days-of-data-engineering-challenge-day-2-data-engineering-vs-data-science-vs-data-analytics-29l4</guid>
      <description>&lt;p&gt;✅ &lt;strong&gt;Why Compare These Roles?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
In modern data teams, Data Engineering, Data Science, and Data Analytics are three core pillars - but many people confuse them.&lt;br&gt;&lt;br&gt;
Knowing who does what:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoids misunderstandings in projects.
&lt;/li&gt;
&lt;li&gt;Helps you choose your career path wisely.
&lt;/li&gt;
&lt;li&gt;Makes collaboration smoother.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🗂️ &lt;strong&gt;The Big Picture&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Focus&lt;/th&gt;
&lt;th&gt;Typical Tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Engineer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Build &amp;amp; manage data pipelines, storage, &amp;amp; processing infrastructure.&lt;/td&gt;
&lt;td&gt;SQL, Python, Spark, Hadoop, Airflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Scientist&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Develop models, run experiments, make predictions.&lt;/td&gt;
&lt;td&gt;Python, R, TensorFlow, Scikit-learn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Analyst&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Analyze data, build reports &amp;amp; dashboards, answer business questions.&lt;/td&gt;
&lt;td&gt;SQL, Excel, Tableau, Power BI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;👉 &lt;strong&gt;Key Difference:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Engineers build the highways.&lt;br&gt;&lt;br&gt;
Scientists build self-driving cars to run on them.&lt;br&gt;&lt;br&gt;
Analysts report on the traffic.&lt;/p&gt;

&lt;p&gt;If you've been wanting to break into data engineering but don't know where to start, this guide gives you a simple, clear path to follow. &lt;strong&gt;&lt;a href="https://pavandeore.gumroad.com/l/data-engineering-complete-roadmap-for-beginners" rel="noopener noreferrer"&gt;Break Into Data Engineering: A Complete Roadmap for Beginners&lt;/a&gt;&lt;/strong&gt; cuts through the noise and explains the essentials in a friendly, beginner-focused way across 15 comprehensive chapters and 190 pages. It's built to help you finally understand the field and know exactly what to learn next.&lt;/p&gt;

&lt;p&gt;⚙️ &lt;strong&gt;What a Data Engineer Does&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Main tasks:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Design data architecture (databases, data lakes, warehouses)
&lt;/li&gt;
&lt;li&gt;Develop, test, and maintain ETL/ELT pipelines
&lt;/li&gt;
&lt;li&gt;Integrate diverse data sources
&lt;/li&gt;
&lt;li&gt;Optimize storage &amp;amp; queries for performance
&lt;/li&gt;
&lt;li&gt;Monitor pipeline health &amp;amp; troubleshoot issues
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key goal:&lt;/strong&gt; Deliver clean, structured, reliable data.&lt;/p&gt;

&lt;p&gt;🔬 &lt;strong&gt;What a Data Scientist Does&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Main tasks:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explore &amp;amp; analyze large data sets
&lt;/li&gt;
&lt;li&gt;Build and test statistical &amp;amp; machine learning models
&lt;/li&gt;
&lt;li&gt;Perform A/B testing &amp;amp; experimentation
&lt;/li&gt;
&lt;li&gt;Interpret results and provide predictions
&lt;/li&gt;
&lt;li&gt;Communicate complex findings to stakeholders
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key goal:&lt;/strong&gt; Turn data into actionable insights &amp;amp; predictive systems.&lt;/p&gt;

&lt;p&gt;📊 &lt;strong&gt;What a Data Analyst Does&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Main tasks:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use SQL &amp;amp; BI tools to answer specific questions
&lt;/li&gt;
&lt;li&gt;Create dashboards and visual reports
&lt;/li&gt;
&lt;li&gt;Identify trends &amp;amp; patterns in historical data
&lt;/li&gt;
&lt;li&gt;Support decision-making with clear insights
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key goal:&lt;/strong&gt; Help teams understand what happened and why.&lt;/p&gt;

&lt;p&gt;🔑 &lt;strong&gt;Real-World Example&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Example: E-commerce company&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1️⃣ Data Engineer:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sets up a pipeline to collect website clicks, purchases, and customer info.
&lt;/li&gt;
&lt;li&gt;Stores it in a data warehouse (e.g., Snowflake).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2️⃣ Data Scientist:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses that clean data to predict which customers are likely to churn.
&lt;/li&gt;
&lt;li&gt;Tests different retention strategies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3️⃣ Data Analyst:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Builds daily reports showing sales trends, customer segments, and marketing campaign performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🎯 &lt;strong&gt;Key Takeaways for Day 2&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
✅ Data Engineers = &lt;strong&gt;Backbone&lt;/strong&gt;: They build and maintain the data foundation.&lt;br&gt;&lt;br&gt;
✅ Data Scientists = &lt;strong&gt;Innovators&lt;/strong&gt;: They create models that predict the future.&lt;br&gt;&lt;br&gt;
✅ Data Analysts = &lt;strong&gt;Explorers&lt;/strong&gt;: They dig into past and present data to provide clear insights.&lt;br&gt;&lt;br&gt;
✅ These roles collaborate, not compete - each is vital for a modern data team.&lt;/p&gt;

&lt;p&gt;🏃‍♂️ &lt;strong&gt;Action Step&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Today's mini-task:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
👉 Make a simple table:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One column: Your current skills
&lt;/li&gt;
&lt;li&gt;Second column: Engineer, Scientist, or Analyst?
Tick what matches best - this helps you see where you fit now and where you want to grow!&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>data</category>
      <category>dataengineering</category>
      <category>programming</category>
      <category>python</category>
    </item>
    <item>
      <title>Data Engineering in 30 Days - Day 2</title>
      <dc:creator>pawan deore</dc:creator>
      <pubDate>Tue, 09 Dec 2025 12:26:27 +0000</pubDate>
      <link>https://dev.to/pawandeore/30-days-of-data-engineering-challenge-day-2-data-engineering-vs-data-science-vs-data-analytics-4nbf</link>
      <guid>https://dev.to/pawandeore/30-days-of-data-engineering-challenge-day-2-data-engineering-vs-data-science-vs-data-analytics-4nbf</guid>
      <description>&lt;h1&gt;
  
  
  Day 2: Data Engineer vs Data Scientist vs Data Analyst — What’s the Difference?
&lt;/h1&gt;

&lt;h2&gt;
  
  
  ✅ Why Compare These Roles?
&lt;/h2&gt;

&lt;p&gt;In modern data teams, &lt;strong&gt;Data Engineering&lt;/strong&gt;, &lt;strong&gt;Data Science&lt;/strong&gt;, and &lt;strong&gt;Data Analytics&lt;/strong&gt; form three essential pillars — yet they’re often misunderstood or mixed up.&lt;/p&gt;

&lt;p&gt;Understanding the differences helps you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoid confusion in projects
&lt;/li&gt;
&lt;li&gt;Choose the right career path
&lt;/li&gt;
&lt;li&gt;Collaborate more effectively
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you’ve been wanting to break into data engineering but don’t know where to start, this guide gives you a simple, clear path to follow. &lt;a href="https://pavandeore.gumroad.com/l/data-engineering-complete-roadmap-for-beginners" rel="noopener noreferrer"&gt;Data Engineering: Complete Roadmap&lt;/a&gt; cuts through the noise and explains the essentials in a friendly, beginner-focused way across 15 comprehensive chapters and 190 pages.&lt;/p&gt;




&lt;h2&gt;
  
  
  🗂️ The Big Picture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Role Comparison Table
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Focus&lt;/th&gt;
&lt;th&gt;Typical Tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Engineer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Build &amp;amp; manage data pipelines, storage, and processing infrastructure&lt;/td&gt;
&lt;td&gt;SQL, Python, Spark, Hadoop, Airflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Scientist&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Develop models, run experiments, make predictions&lt;/td&gt;
&lt;td&gt;Python, R, TensorFlow, Scikit-learn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Analyst&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Analyze data, build reports &amp;amp; dashboards, answer business questions&lt;/td&gt;
&lt;td&gt;SQL, Excel, Tableau, Power BI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  👉 Key Difference (Simple Analogy)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data Engineers&lt;/strong&gt; build the &lt;strong&gt;highways&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Scientists&lt;/strong&gt; build the &lt;strong&gt;self-driving cars&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Analysts&lt;/strong&gt; report on the &lt;strong&gt;traffic&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📘 Want to Break Into Data Engineering?
&lt;/h2&gt;

&lt;p&gt;If you’ve been wanting to break into data engineering but don’t know where to start, this guide lays out a super clean path:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Break Into Data Engineering: A Complete Roadmap for Beginners&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A friendly, 190-page beginner-focused book covering the essentials in 15 structured chapters.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ What a Data Engineer Does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Main tasks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Design data architecture (databases, data lakes, warehouses)
&lt;/li&gt;
&lt;li&gt;Develop, test, and maintain ETL/ELT pipelines
&lt;/li&gt;
&lt;li&gt;Integrate diverse data sources
&lt;/li&gt;
&lt;li&gt;Optimize storage &amp;amp; queries for performance
&lt;/li&gt;
&lt;li&gt;Monitor pipeline health &amp;amp; troubleshoot issues
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key goal:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Deliver clean, structured, reliable data.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🔬 What a Data Scientist Does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Main tasks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explore &amp;amp; analyze large datasets
&lt;/li&gt;
&lt;li&gt;Build and test statistical &amp;amp; machine learning models
&lt;/li&gt;
&lt;li&gt;Run A/B tests &amp;amp; experiments
&lt;/li&gt;
&lt;li&gt;Interpret and communicate findings
&lt;/li&gt;
&lt;li&gt;Provide predictions &amp;amp; insights
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key goal:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Turn data into actionable insights and predictive systems.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 What a Data Analyst Does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Main tasks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use SQL &amp;amp; BI tools to answer specific questions
&lt;/li&gt;
&lt;li&gt;Create dashboards &amp;amp; visual reports
&lt;/li&gt;
&lt;li&gt;Identify trends in historical data
&lt;/li&gt;
&lt;li&gt;Support decisions with clear insights
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key goal:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Help teams understand what happened and why.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🔑 Real-World Example: E-Commerce Company
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1️⃣ Data Engineer
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Builds pipelines to collect website clicks, orders, and customer data
&lt;/li&gt;
&lt;li&gt;Loads everything into a data warehouse (e.g., Snowflake)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2️⃣ Data Scientist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Uses the cleaned data to predict churn
&lt;/li&gt;
&lt;li&gt;Tests retention strategies&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3️⃣ Data Analyst
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Produces daily dashboards for sales, customer segments, and marketing performance
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎯 Key Takeaways for Day 2
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data Engineers = Backbone&lt;/strong&gt; → They build and maintain the data foundation
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Scientists = Innovators&lt;/strong&gt; → They create models that predict the future
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Analysts = Explorers&lt;/strong&gt; → They uncover insights from past &amp;amp; present data
&lt;/li&gt;
&lt;li&gt;These roles &lt;strong&gt;collaborate&lt;/strong&gt;, not compete — each is essential in modern teams
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🏃‍♂️ Action Step
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Today’s mini-task:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;👉 Create a simple two-column table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your Current Skills&lt;/th&gt;
&lt;th&gt;Engineer / Scientist / Analyst?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mark where you fit today — this gives clarity on where you might want to grow!&lt;/p&gt;




&lt;p&gt;Start your data engineering journey with &lt;a href="https://pavandeore.gumroad.com/l/data-engineering-complete-roadmap-for-beginners" rel="noopener noreferrer"&gt;Data Engineering: A Complete Roadmap for Beginners&lt;/a&gt; cuts through the noise and explains the essentials in a friendly, beginner-focused way across 15 comprehensive chapters and 190 pages.&lt;/p&gt;




</description>
      <category>dataengineering</category>
      <category>data</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>End-to-End LLM Project Natural Language to SQL Application</title>
      <dc:creator>pawan deore</dc:creator>
      <pubDate>Sat, 16 Aug 2025 13:37:03 +0000</pubDate>
      <link>https://dev.to/pawandeore/end-to-end-llm-project-natural-language-to-sql-application-1bgi</link>
      <guid>https://dev.to/pawandeore/end-to-end-llm-project-natural-language-to-sql-application-1bgi</guid>
      <description>&lt;p&gt;FREE Course Link - &lt;a href="https://www.udemy.com/course/end-to-end-llm-powered-natural-language-to-sql-application/" rel="noopener noreferrer"&gt;https://www.udemy.com/course/end-to-end-llm-powered-natural-language-to-sql-application/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This course walks you through building a fully functional, LLM-powered application that transforms natural language into SQL queries and returns real-time results from a connected database. Designed for developers and AI enthusiasts, it focuses on real backend systems, LLM integration, and query logic—no deployment or theory-heavy content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Introduction to LLM-Powered Applications&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Learn the key concepts behind using Large Language Models like GPT for SQL generation. Understand the application architecture and flow: from natural language input to query execution and result rendering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setting Up Flask and Database Connections&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Build the backend using Flask. Learn how to securely gather and store user connection info (host, port, user, password) and connect to PostgreSQL, MySQL, or SQLite using SQLAlchemy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integrating OpenAI API for SQL Generation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use the OpenAI API to turn natural language queries into syntactically correct SQL. You'll design prompts, configure models, and handle API response parsing and error handling in your Python code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extracting Database Schema for Contextual Queries&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Dynamically fetch and format schema information for more accurate SQL generation. Explore how to retrieve table names and column data types from your connected database to guide LLM output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Improving Prompt Engineering and Query Accuracy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Refine how prompts are structured based on schema info and query intent. Add custom logic for PostgreSQL-specific behaviors like case sensitivity and identifier quoting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Designing a Functional Web Interface with Flask&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a simple HTML interface for inputting queries and displaying SQL results. Learn how to maintain session state, format results, and handle common user and database errors.&lt;/p&gt;

&lt;p&gt;By the end, you'll have a complete AI-powered SQL interface, suitable for internal tools, BI assistants, or learning projects.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Data Science Projects You can start this weekend</title>
      <dc:creator>pawan deore</dc:creator>
      <pubDate>Sun, 29 Jun 2025 15:32:44 +0000</pubDate>
      <link>https://dev.to/pawandeore/data-science-projects-you-can-start-this-weekend-14dl</link>
      <guid>https://dev.to/pawandeore/data-science-projects-you-can-start-this-weekend-14dl</guid>
      <description>&lt;p&gt;Looking for some hands-on data science projects to sharpen your skills this weekend? Whether you're a beginner or an experienced practitioner, working on real-world projects is one of the best ways to learn. Below, we've curated 10 fantastic data science projects from a comprehensive list, spanning various domains like NLP, computer vision, time series forecasting, and MLOps.&lt;/p&gt;

&lt;p&gt;Each project comes with a clear objective, relevant technologies, and a link to detailed instructions—so you can dive right in!&lt;/p&gt;

&lt;p&gt;🔥 1. Digit Recognition using CNN for MNIST Dataset&lt;br&gt;
Domain: Computer Vision / Deep Learning&lt;br&gt;
Tech Stack: Python, TensorFlow/Keras, CNN&lt;/p&gt;

&lt;p&gt;Why Try This?&lt;br&gt;
The MNIST dataset is perfect for beginners to explore Convolutional Neural Networks (CNNs). You'll learn how to preprocess image data, build a CNN model, and evaluate its performance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.projectpro.io/project-use-case/digit-recognizer-using-cnn?utm_source=pawan&amp;amp;utm_medium=devto" rel="noopener noreferrer"&gt;🔗 Project Link&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;📊 2. Time Series Forecasting with Facebook Prophet&lt;br&gt;
Domain: Time Series Analysis&lt;br&gt;
Tech Stack: Python, Facebook Prophet, Cesium&lt;/p&gt;

&lt;p&gt;Why Try This?&lt;br&gt;
Time series forecasting is crucial in finance, sales, and IoT. This project teaches you how to use Facebook Prophet, a powerful forecasting tool by Meta, to predict future trends.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.projectpro.io/project-use-case/facebook-prophet-time-series-python?utm_source=pawan&amp;amp;utm_medium=devto" rel="noopener noreferrer"&gt;🔗 Project Link&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;🤖 3. Text Classification with Transformers (RoBERTa &amp;amp; XLNet)&lt;br&gt;
Domain: NLP / Transformers&lt;br&gt;
Tech Stack: Python, Hugging Face, PyTorch&lt;/p&gt;

&lt;p&gt;Why Try This?&lt;br&gt;
Transformers like RoBERTa and XLNet dominate NLP tasks. This project walks you through fine-tuning these models for text classification, a skill useful in sentiment analysis, spam detection, and more.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.projectpro.io/project-use-case/text-classification-using-transformer-models-roberta-and-xlnet?utm_source=pawan&amp;amp;utm_medium=devto" rel="noopener noreferrer"&gt;🔗 Project Link&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;🛒 4. Market Basket Analysis using Apriori &amp;amp; FP-Growth&lt;br&gt;
Domain: Recommendation Systems&lt;br&gt;
Tech Stack: Python, Scikit-learn, Pandas&lt;/p&gt;

&lt;p&gt;Why Try This?&lt;br&gt;
Ever wondered how Amazon recommends products? This project uses association rule mining (Apriori &amp;amp; FP-Growth) to uncover product purchase patterns—essential for retail analytics.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.projectpro.io/project-use-case/market-basket-analysis-apriori-fpgrowth?utm_source=pawan&amp;amp;utm_medium=devto" rel="noopener noreferrer"&gt;🔗 Project Link&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;📈 5. Loan Default Prediction with Explainable AI&lt;br&gt;
Domain: Finance / ML Interpretability&lt;br&gt;
Tech Stack: Python, LightGBM, SHAP&lt;/p&gt;

&lt;p&gt;Why Try This?&lt;br&gt;
Banks need to understand why a loan might default. This project combines LightGBM with SHAP values to build a model that’s both accurate and interpretable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.projectpro.io/project-use-case/loan-default-prediction-explainable-ai?utm_source=pawan&amp;amp;utm_medium=devto" rel="noopener noreferrer"&gt;🔗 Project Link&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;🏡 6. House Price Prediction with Regression Models&lt;br&gt;
Domain: Regression / Predictive Analytics&lt;br&gt;
Tech Stack: Python, Scikit-learn, Pandas&lt;/p&gt;

&lt;p&gt;Why Try This?&lt;br&gt;
A classic ML project! Predict house prices using linear regression, Ridge, and Lasso, while learning feature engineering and model evaluation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.projectpro.io/project-use-case/house-price-prediction-project-using-machine-learning-regression?utm_source=pawan&amp;amp;utm_medium=devto" rel="noopener noreferrer"&gt;🔗 Project Link&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;🚀 7. Deploy an ML Model with Streamlit &amp;amp; PyCaret&lt;br&gt;
Domain: MLOps / Deployment&lt;br&gt;
Tech Stack: Python, PyCaret, Streamlit&lt;/p&gt;

&lt;p&gt;Why Try This?&lt;br&gt;
Model deployment is a must-have skill. This project shows how to build and deploy an ML app quickly using PyCaret for automation and Streamlit for the UI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.projectpro.io/project-use-case/deploy-a-ml-app-using-pycaret-and-streamlit?utm_source=pawan&amp;amp;utm_medium=devto" rel="noopener noreferrer"&gt;🔗 Project Link&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;🎭 8. Fake News Detection with NLP &amp;amp; Deep Learning&lt;br&gt;
Domain: NLP / Deep Learning&lt;br&gt;
Tech Stack: Python, TensorFlow, LSTM&lt;/p&gt;

&lt;p&gt;Why Try This?&lt;br&gt;
Fake news is a growing problem. Learn how to classify news articles as real or fake using LSTM networks, a type of recurrent neural network (RNN).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.projectpro.io/project-use-case/sequence-classification-with-lstm-rnn-in-python?utm_source=pawan&amp;amp;utm_medium=devto" rel="noopener noreferrer"&gt;🔗 Project Link&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;🛠️ 9. Build a CI/CD Pipeline for ML with Jenkins&lt;br&gt;
Domain: MLOps / Automation&lt;br&gt;
Tech Stack: Jenkins, Docker, Python&lt;/p&gt;

&lt;p&gt;Why Try This?&lt;br&gt;
CI/CD pipelines automate ML workflows. This project teaches you how to set up Jenkins for ML model testing and deployment, a valuable skill in production environments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.projectpro.io/project-use-case/ci-cd-pipeline-for-machine-learning-projects-using-jenkins?utm_source=pawan&amp;amp;utm_medium=devto" rel="noopener noreferrer"&gt;🔗 Project Link&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;🏎️ 10. Real-Time Streaming Pipeline with Spark &amp;amp; Kafka&lt;br&gt;
Domain: Big Data / Real-Time Analytics&lt;br&gt;
Tech Stack: PySpark, Kafka, AWS&lt;/p&gt;

&lt;p&gt;Why Try This?&lt;br&gt;
Real-time data processing is key in IoT and finance. This project guides you in building a Spark Streaming pipeline with Kafka for live data analysis.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.projectpro.io/project-use-case/real-time-streaming-data-pipeline-using-apache-flink-python-and-amazon-kinesis?utm_source=pawan&amp;amp;utm_medium=devto" rel="noopener noreferrer"&gt;🔗 Project Link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These projects cover diverse data science domains—from NLP and computer vision to MLOps and big data. Pick one that excites you and start coding this weekend!&lt;/p&gt;

&lt;p&gt;💡 Pro Tip: If you're a beginner, start with MNIST Digit Recognition or House Price Prediction. If you're advanced, try Transformer-based NLP models or real-time Spark pipelines.&lt;/p&gt;

&lt;p&gt;Happy coding! 🚀&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>python</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Data Engineering in 30 Days: Day 1</title>
      <dc:creator>pawan deore</dc:creator>
      <pubDate>Thu, 19 Jun 2025 03:05:29 +0000</pubDate>
      <link>https://dev.to/pawandeore/data-engineering-in-30-days-day-1-380o</link>
      <guid>https://dev.to/pawandeore/data-engineering-in-30-days-day-1-380o</guid>
      <description>&lt;h2&gt;
  
  
  ✅ What is Data Engineering?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Data Engineering&lt;/strong&gt; is the discipline focused on designing, building, and maintaining systems and pipelines that collect, store, process, and deliver data reliably and efficiently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key ideas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It transforms raw data into usable data for analytics and machine learning.&lt;/li&gt;
&lt;li&gt;It handles big volumes of data (terabytes to petabytes).&lt;/li&gt;
&lt;li&gt;It ensures data is clean, consistent, and available to the right people and systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ⚙️ Why is Data Engineering Important?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Without data engineering:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data is messy, scattered, and unreliable.&lt;/li&gt;
&lt;li&gt;Analysts and data scientists waste time cleaning data instead of extracting insights.&lt;/li&gt;
&lt;li&gt;Companies struggle to make data-driven decisions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;With good data engineering:&lt;/strong&gt;&lt;br&gt;
✅ Business decisions are based on high-quality data.&lt;br&gt;&lt;br&gt;
✅ Data is fresh, trustworthy, and accessible.&lt;br&gt;&lt;br&gt;
✅ Complex analytics, dashboards, and ML models run smoothly.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In short:&lt;/strong&gt; Data engineers build the foundation for all modern data-driven work.&lt;/p&gt;




&lt;p&gt;If you’ve been wanting to break into data engineering but don’t know where to start, this guide gives you a simple, clear path to follow. &lt;a href="https://pavandeore.gumroad.com/l/data-engineering-complete-roadmap-for-beginners" rel="noopener noreferrer"&gt;Data Engineering: A Complete Roadmap&lt;/a&gt; cuts through the noise and explains the essentials in a friendly, beginner-focused way across 15 comprehensive chapters and 190 pages.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔑 Typical Tasks of a Data Engineer
&lt;/h2&gt;

&lt;p&gt;Here’s what data engineers do daily:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build scalable pipelines:&lt;/strong&gt; Automate the flow of data from multiple sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrate various systems:&lt;/strong&gt; APIs, databases, IoT devices, and external feeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean and transform data:&lt;/strong&gt; Fix errors, standardize formats, enrich data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design storage solutions:&lt;/strong&gt; Databases, data lakes, and data warehouses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ensure security and governance:&lt;/strong&gt; Control access and comply with privacy laws.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor and maintain pipelines:&lt;/strong&gt; Automate alerts and handle failures gracefully.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🗂️ Core Components in a Data Engineering Workflow
&lt;/h2&gt;

&lt;p&gt;1️⃣ &lt;strong&gt;Data Sources:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
APIs, transactional databases, server logs, sensors, third-party data.&lt;/p&gt;

&lt;p&gt;2️⃣ &lt;strong&gt;Ingestion Layer:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Tools like Apache NiFi, Kafka, or custom scripts to bring in data.&lt;/p&gt;

&lt;p&gt;3️⃣ &lt;strong&gt;Storage Layer:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Relational Databases (PostgreSQL, MySQL)
&lt;/li&gt;
&lt;li&gt;NoSQL Databases (MongoDB, Cassandra)
&lt;/li&gt;
&lt;li&gt;Data Warehouses (Snowflake, Redshift, BigQuery)
&lt;/li&gt;
&lt;li&gt;Data Lakes (AWS S3, Hadoop HDFS)
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;4️⃣ &lt;strong&gt;Processing Layer:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Batch processing — Spark, Hadoop
&lt;/li&gt;
&lt;li&gt;Streaming processing — Kafka Streams, Flink
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;5️⃣ &lt;strong&gt;Orchestration:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Workflow scheduling with Apache Airflow, Luigi.&lt;/p&gt;

&lt;p&gt;6️⃣ &lt;strong&gt;Monitoring &amp;amp; Logging:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Set up alerts, logs, and dashboards to keep pipelines healthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧰 Key Skills &amp;amp; Tools to Learn
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Programming Languages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python:&lt;/strong&gt; Most popular for scripting, ETL jobs, and working with frameworks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL:&lt;/strong&gt; Querying databases is a must-have skill.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Frameworks &amp;amp; Tools:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apache Spark: For large-scale batch &amp;amp; stream processing.&lt;/li&gt;
&lt;li&gt;Hadoop: Distributed storage &amp;amp; processing.&lt;/li&gt;
&lt;li&gt;Apache Airflow: Schedule &amp;amp; orchestrate data workflows.&lt;/li&gt;
&lt;li&gt;dbt (Data Build Tool): For managing transformations in the warehouse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cloud Platforms:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS (Glue, EMR, Redshift, S3)
&lt;/li&gt;
&lt;li&gt;Google Cloud (BigQuery, Dataflow)
&lt;/li&gt;
&lt;li&gt;Azure (Data Factory, Synapse)
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📈 Example: How a Data Pipeline Works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Scenario:&lt;/strong&gt; A company wants daily sales dashboards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pipeline Steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Extract:&lt;/strong&gt; Pull raw sales transactions from the store’s POS database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transform:&lt;/strong&gt; Clean data — fix missing values, convert currencies, join with product info.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load:&lt;/strong&gt; Store cleaned data into a data warehouse like Snowflake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serve:&lt;/strong&gt; Analysts and BI tools (e.g., Tableau, Power BI) query this warehouse for reports.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;✅ &lt;strong&gt;Automation ensures this happens daily with no manual work!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 Key Takeaways for Day 1
&lt;/h2&gt;

&lt;p&gt;✅ Data Engineering is the backbone of all analytics and AI work.&lt;br&gt;&lt;br&gt;
✅ It combines coding, system design, and an understanding of business data needs.&lt;br&gt;&lt;br&gt;
✅ Focus on building clean, reliable, and scalable pipelines.&lt;br&gt;&lt;br&gt;
✅ Start by mastering SQL, Python, and a basic ETL pipeline.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>data</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>NumPy Interview Questions Practice Test Series</title>
      <dc:creator>pawan deore</dc:creator>
      <pubDate>Sat, 17 May 2025 03:49:59 +0000</pubDate>
      <link>https://dev.to/pawandeore/numpy-interview-questions-practice-test-series-59nj</link>
      <guid>https://dev.to/pawandeore/numpy-interview-questions-practice-test-series-59nj</guid>
      <description>&lt;p&gt;NumPy Interview Questions Practice Test Series - &lt;a href="https://www.udemy.com/course/numpy-interview-questions-practice-test-series/?referralCode=B14F12CEE8D380F896F3" rel="noopener noreferrer"&gt;Complete List&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>python</category>
      <category>datascience</category>
      <category>numpy</category>
    </item>
    <item>
      <title>Created Chrome Extension - Job Application Helper</title>
      <dc:creator>pawan deore</dc:creator>
      <pubDate>Sun, 11 May 2025 09:10:45 +0000</pubDate>
      <link>https://dev.to/pawandeore/created-chrome-extension-job-application-helper-269k</link>
      <guid>https://dev.to/pawandeore/created-chrome-extension-job-application-helper-269k</guid>
      <description>&lt;p&gt;Generates tailored responses to generic questions based on job descriptions and your resume&lt;/p&gt;

&lt;p&gt;How It Works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enter Your Details – Provide your resume summary and the job description.&lt;/li&gt;
&lt;li&gt;Generate Answers – Click the button to get AI-crafted responses in seconds.&lt;/li&gt;
&lt;li&gt;Refine &amp;amp; Use – Copy the answers and personalize them further if needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Chrome Extension Link - &lt;a href="https://chromewebstore.google.com/detail/hkkhnjpkocafphamlikgmfhdpiknelpn?utm_source=item-share-cb" rel="noopener noreferrer"&gt;Job Application Helper&lt;/a&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>interview</category>
      <category>programming</category>
      <category>resume</category>
    </item>
  </channel>
</rss>
