Hello! In our previous lesson, we contrasted batch and streaming data processing, concluding that your pipeline will benefit from a hybrid approach. We used batch processing for tasks like periodic API and CSV ingestion, and stream processing for real-time data from Kafka. We also touched upon a crucial question: what happens if a task fails and needs to be re-run? A simple retry could lead to duplicate data, corrupting your analytics dashboard and NLP inputs.
This lesson directly addresses that challenge by introducing idempotency, a fundamental concept for building reliable and fault-tolerant systems. We will define idempotency in the context of data pipelines, explore why it's non-negotiable for data integrity, and examine practical techniques for implementing it, particularly with your chosen stack of Python and PostgreSQL.
By the end of this lesson, you will be able to explain what idempotency is, why it is so critical for pipeline reliability, and how you can design your ingestion processes to be idempotent from the very beginning.
What is Idempotency and Why is it Critical?
At its core, an operation is idempotent if executing it multiple times produces the same result as executing it just once. Think of pressing an elevator button for your floor. After the first press, the button lights up, and the call is registered. Pressing it again ten more times doesn't call the elevator ten more times; the system's state remains the same as after the first press.
In data pipelines, this property is not just a nice-to-have; it's a necessity for reliability. Let's look at a visual comparison.

As the diagram shows, a non-idempotent pipeline duplicates data on a re-run. An idempotent pipeline correctly handles the re-run, ensuring the final state is identical to what a single successful run would have produced.
So, why are these re-runs so common? The following article highlights the typical failure scenarios that make idempotency essential.
Idempotency in Data Pipelines: How to Prevent Duplicate Records - DEV Community
This article from the DEV Community clearly defines idempotency for data pipelines and outlines the real-world situations that cause data duplication.
Please read the first three sections of the article to understand the core problem: Start with the introduction, which defines the concept. Read the next section for a concise contrast between idempotent and non-idempotent operations. Finally, focus on the section Common Sources of Duplicate Records. This is the "why" – it explains the inevitable real-world events that will break a non-idempotent pipeline.
The article highlights three key sources of failure:
- Partial Success and Retries: Your Airflow job ingesting 100,000 records fails after processing 80,000 due to a network error. When Airflow retries the task, a non-idempotent design would re-process and re-insert those first 80,000 records, creating duplicates.
- Parallel Execution: A scheduler might mistakenly launch a second instance of a job that is already running, causing two processes to write the same data simultaneously.
- Checkpoint Failures: A streaming consumer reads a batch of messages from Kafka, writes them to PostgreSQL, but crashes before it can commit the offset (the "checkpoint") back to Kafka. On restart, the consumer will re-read and re-process the exact same messages.
In all these cases, the system is simply trying to recover from a failure. Without an idempotent design, this legitimate recovery action leads to data corruption.
Achieving Idempotency: The Upsert Pattern
So, how do we build idempotent pipelines? The most robust and common method at the data storage layer is the upsert pattern: UPdate the record if it already exists, otherwise INSert it.
To understand the logic, let's watch a short video that uses a simple analogy.
This clip from a CockroachDB video uses a great analogy to explain the core idea of idempotency.
Watch the section from the beginning. Pay attention to the comparison between adding an element to a list versus adding it to a set.
A database table with a unique key constraint behaves like a set. You can try to add the same item multiple times, but it will only ever exist once. This is the foundation of the upsert. To perform an upsert, the database needs to know how to uniquely identify a record. This is done using a natural key or a primary key.
Since your project uses PostgreSQL, you can implement this with the INSERT ... ON CONFLICT statement. This is a powerful, native feature for handling upserts.
Idempotency in Data Pipelines: How to Prevent Duplicate Records - DEV Community
Let's return to the DEV Community article to see how this is implemented in practice.
Read the section Upsert as the Foundation. Then, study the PostgreSQL example provided. Notice how ON CONFLICT (event_id) tells the database which column to use to detect a duplicate.
The SQL pattern for PostgreSQL looks like this:
INSERT INTO your_table (unique_id, data_column_1, data_column_2)
VALUES ('some_id', 'value1', 'value2')
ON CONFLICT (unique_id) DO UPDATE SET
data_column_1 = EXCLUDED.data_column_1,
data_column_2 = EXCLUDED.data_column_2;
Here, unique_id is the column with a unique constraint. If an INSERT is attempted with an unique_id that already exists, the DO UPDATE clause is executed instead, updating the existing record with the new values from the EXCLUDED pseudo-table. If the unique_id is new, a simple insert occurs. This single atomic operation guarantees that a record is either inserted or updated, but never duplicated.
When Source Data Lacks a Unique Key
The ON CONFLICT strategy works perfectly when your source data provides a stable, unique identifier for each record (e.g., an order_id, user_id, or event_id). But what if it doesn't? This is common with data from CSV files or simple APIs.
The solution is to create a deterministic unique ID ourselves. Deterministic means that for the same input record, we must always generate the exact same ID. This is typically done by creating a hash from the content of the fields that uniquely identify a record.
Given your proficiency in Python, you can easily implement this using standard libraries.
Idempotency in Data Pipelines: How to Prevent Duplicate Records - DEV Community
The same article provides a practical Python solution for this exact problem.
Read the section titled Deterministic ID Generation. Pay close attention to the Python code snippet.
The logic is as follows:
- Identify a combination of columns that, together, are unique for each record (e.g.,
user_emailandtransaction_timestamp). - Create a canonical (standardized) representation of this data, for instance by creating a sorted JSON string.
- Use a hashing algorithm like SHA-256 to compute a unique fingerprint of that string.
- Use this generated hash as the
unique_idin your target PostgreSQL table.
Now, even if a record is processed twice, your Python script will generate the exact same hash both times, allowing the INSERT ... ON CONFLICT statement to correctly identify it as a duplicate and perform an update instead of a duplicate insert.
Idempotency in the Broader Pipeline
While database upserts are a cornerstone, idempotency is a principle that applies across your entire pipeline.
-
API Calls: Since you'll be ingesting data from APIs, it's important to know about HTTP idempotency. Many modern APIs allow you to pass an "Idempotency-Key" in the request header. If you make a request (e.g., to create a resource) and it times out, you can safely retry with the same key. The server will recognize the key, see that it already processed the original request, and simply return the original successful response without creating a second resource.
-
Batch Processing by Time Window: For batch jobs that process data in time windows (e.g., "process all data from yesterday"), a simple idempotent strategy is truncate and reload. Before loading yesterday's data, you first run a
DELETE FROM your_table WHERE date_column = 'yesterday'command. This ensures that a re-run of the job for the same day will always result in a correct state.
Key Takeaways
- Idempotency means an operation can be re-run multiple times without changing the result beyond the initial application.
- It is critical for reliability because failures, timeouts, and retries are normal and expected events in distributed systems like data pipelines.
- The primary pattern for achieving idempotency when loading data is the upsert, implemented in PostgreSQL with
INSERT ... ON CONFLICT. - Upserts rely on a stable, unique key. If the source doesn't provide one, you must generate one deterministically (e.g., by hashing identifying fields).
- Idempotency is a core design principle, not an afterthought. Building it in from the start is far easier than remediating duplicate data in production.
Next time
We have now covered the "what" (ELT), the "when" (batch vs. streaming), and the "how" of reliable loading (idempotency). We are ready to put these principles into a structured architecture. In our next lesson, we will introduce the medallion architecture (bronze, silver, gold), a widely adopted design pattern for layering data in a data lake or warehouse. You'll see how our idempotent loading patterns are the key to creating a reliable "bronze" layer, which is the foundation for all subsequent transformations.