Data Processing Pipelines

ETL, ELT, and batch processing workflows

Moving and Transforming Data

Data pipelines are automated workflows that move data from source to destination, applying transformations along the way. They're the backbone of modern data platforms, taking raw data from databases, APIs, files, and streams, then cleaning, enriching, and preparing it for analytics. Understanding ETL vs ELT, batch vs streaming, and orchestration patterns is essential for building reliable data systems.

What is a Data Pipeline?

A data pipeline is a series of data processing steps where the output of one step becomes the input of the next. Think of it like an assembly line for data.

┌──────────┐    ┌──────────┐    ┌───────────┐    ┌─────────┐
│ EXTRACT  │───►│TRANSFORM │───►│   LOAD    │───►│ CONSUME │
│ (Sources)│    │ (Clean,  │    │(Target DB)│    │ (BI, ML)│
│          │    │  Enrich) │    │           │    │         │
└──────────┘    └──────────┘    └───────────┘    └─────────┘
     ▲                                                  │
     │                                                  │
     └──────────────────────────────────────────────────┘
                    Optional: Feedback Loop
Data flows from source through transformations to destination

Key Components

📥 Extract

Pull data from sources (databases, APIs, files, streams)

⚙️ Transform

Clean, enrich, aggregate, join, filter data

📤 Load

Write to destination (warehouse, lake, database)

ETL vs ELT: Order Matters

The difference between ETL and ELT is when transformation happens, before or after loading.

ETL: Extract, Transform, Load

Traditional approach, clean data before storing

Transform data in a staging area or processing engine BEFORE loading into the target system.

┌──────────┐    ┌──────────────┐    ┌──────────────┐
│ SOURCE   │───►│   STAGING    │───►│   TARGET     │
│ Database │    │   AREA       │    │  Warehouse   │
│          │    │              │    │              │
│ Raw      │    │ Transform:   │    │ Clean,       │
│ Messy    │    │ • Clean      │    │ Structured   │
│ Data     │    │ • Validate   │    │ Data         │
│          │    │ • Enrich     │    │              │
└──────────┘    └──────────────┘    └──────────────┘
Data is cleaned in staging before entering the warehouse
Example: ETL with Python
import pandas as pd

# EXTRACT: Read from source
df = pd.read_csv('sales_data.csv')
# Raw: 10,000 rows with duplicates, nulls, bad data

# TRANSFORM: Clean and enrich
df = df.drop_duplicates()               # Remove duplicates
df = df.dropna(subset=['customer_id'])  # Remove nulls
df['amount'] = df['amount'].abs()       # Fix negative amounts
df['date'] = pd.to_datetime(df['date']) # Parse dates
df = df[df['amount'] > 0]               # Filter valid transactions

# Enrich with customer data
customers = pd.read_sql("SELECT * FROM customers", db_conn)
df = df.merge(customers, on='customer_id', how='left')

# LOAD: Write cleaned data to warehouse
df.to_sql('clean_sales', warehouse_conn, if_exists='append')
Result:
10,000 → 8,734 rows loaded (duplicates removed, invalid filtered)
Warehouse receives only clean, validated data
When to Use ETL
  • Target system has limited compute (can't handle transformations)
  • Need to minimize storage in expensive warehouse
  • Data requires complex transformations
  • Compliance requires data masking before storage
  • Multiple sources need integration before loading
Pros & Cons
✅ Pros
  • Only clean data in warehouse
  • Less storage needed
  • Mature tools and practices
  • Data privacy/security easier
❌ Cons
  • Slower (transform before load)
  • Requires separate staging infrastructure
  • Can't query raw data later
  • Inflexible, hard to change transforms

ELT: Extract, Load, Transform

Modern approach, load raw, transform in target

Load raw data into the target system FIRST, then transform it using the target's compute power.

┌──────────┐    ┌────────────────────────────┐
│ SOURCE   │───►│   TARGET (Data Lake /      │
│ Database │    │   Warehouse)               │
│          │    │                            │
│ Raw      │    │  RAW ZONE                  │
│ Data     │    │  • Store everything        │
│          │    │                            │
└──────────┘    │  ↓ Transform using SQL,    │
                │    Spark, dbt              │
                │                            │
                │  TRANSFORMED ZONE          │
                │  • Cleaned, enriched data  │
                └────────────────────────────┘
Raw data loaded first, transformed inside the warehouse/lake
Example: ELT with dbt
-- EXTRACT & LOAD: Use Fivetran/Airbyte to load raw data
-- Raw table already exists in warehouse: raw_sales

-- TRANSFORM: dbt model (runs in warehouse)
-- models/staging/stg_sales.sql
WITH source AS (
    SELECT * FROM raw_sales
),

cleaned AS (
    SELECT DISTINCT
        customer_id,
        ABS(amount) AS amount,
        CAST(date AS DATE) AS date
    FROM source
    WHERE customer_id IS NOT NULL
      AND amount > 0
)

SELECT
    c.*,
    cust.name AS customer_name,
    cust.segment
FROM cleaned c
LEFT JOIN dim_customers cust
    ON c.customer_id = cust.customer_id
Result:
Transformed table created in warehouse using SQL
Leverages warehouse compute (Snowflake, BigQuery, etc.)
When to Use ELT
  • Target system has powerful compute (cloud warehouses)
  • Storage is cheap (data lakes)
  • Need flexibility to re-transform raw data
  • Want faster data availability (load quickly, transform later)
  • Using modern cloud data platforms
Pros & Cons
✅ Pros
  • Faster to get data available
  • Can re-transform raw data anytime
  • Leverages warehouse compute
  • More flexible and agile
  • Simpler architecture
❌ Cons
  • More storage needed (raw + transformed)
  • Requires powerful target system
  • Costs more in compute
  • Raw data may have PII/sensitive info
💡 Modern Trend: ELT is becoming the default for cloud-based architectures. Cloud warehouses (Snowflake, BigQuery, Redshift) have massive compute power, making in-database transformations faster and more cost-effective than external ETL servers.

Batch vs Streaming Processing

Data pipelines can process data in batches (periodic chunks) or streams (continuous flow).

Batch Processing

Process data in scheduled intervals (hourly, daily, weekly). Collects data over time, then processes it all at once.

Time:     00:00        01:00        02:00        03:00
          │            │            │            │
Data:     ████████     ████████     ████████     ████████
          Collect      Collect      Collect      Collect
                       │
                       ▼
                    Process
                    All Data
                    From Last Hour
Data accumulates, then processes in bulk at scheduled times
Example: Daily Batch Job
# Airflow DAG - runs daily at 2 AM
from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime, timedelta

def extract_daily_sales():
    """Extract yesterday's sales"""
    query = """
        SELECT * FROM sales
        WHERE date = CURRENT_DATE - 1
    """
    df = pd.read_sql(query, source_db)
    df.to_parquet('/data/raw/sales_{{ ds }}.parquet')

def transform_and_load():
    """Transform and load to warehouse"""
    df = pd.read_parquet('/data/raw/sales_{{ ds }}.parquet')

    # Aggregate by customer
    daily_summary = df.groupby('customer_id').agg({
        'amount': 'sum',
        'quantity': 'sum'
    }).reset_index()

    daily_summary.to_sql('daily_sales_summary', warehouse,
                         if_exists='append')

dag = DAG(
    'daily_sales_pipeline',
    start_date=datetime(2024, 1, 1),
    schedule_interval='0 2 * * *',  # 2 AM daily
)

extract = PythonOperator(task_id='extract', python_callable=extract_daily_sales)
transform = PythonOperator(task_id='transform', python_callable=transform_and_load)

extract >> transform
Result:
Runs every day at 2 AM, processes previous day's data
Latency: ~2-26 hours depending on when transaction occurred
Best For
  • Historical analysis and reporting
  • Large-scale data processing
  • Cost-effective (process when resources are cheap)
  • When real-time isn't required
  • Complex transformations needing full dataset

Streaming Processing

Process data continuously as it arrives. Each event is processed immediately or in micro-batches (seconds).

Time:     00:00:00  00:00:01  00:00:02  00:00:03
          │         │         │         │
Data:     █ ──► Process
             █ ──► Process
                █ ──► Process
                   █ ──► Process

          Continuous flow, each event processed immediately
Data processed as it arrives, near real-time
Example: Kafka Stream Processing
from kafka import KafkaConsumer
import json

consumer = KafkaConsumer(
    'sales_events',
    bootstrap_servers=['localhost:9092'],
    value_deserializer=lambda m: json.loads(m.decode('utf-8'))
)

# Process each event as it arrives
for message in consumer:
    event = message.value

    # Transform in real-time
    processed = {
        'customer_id': event['customer_id'],
        'amount': abs(float(event['amount'])),
        'timestamp': event['timestamp'],
        'category': categorize_purchase(event['amount'])
    }

    # Write to real-time dashboard database
    write_to_db(processed)

    # Check for fraud in real-time
    if event['amount'] > 10000:
        send_fraud_alert(event)
Result:
Each transaction processed within milliseconds
Fraud alerts sent immediately, dashboard updates in real-time
Best For
  • Real-time dashboards and alerts
  • Fraud detection
  • Live recommendations
  • IoT sensor monitoring
  • Financial trading systems
  • Click-stream analysis
AspectBatchStreaming
LatencyMinutes to hoursMilliseconds to seconds
CostLower (scheduled resources)Higher (always-on infrastructure)
ComplexitySimplerMore complex
Use CaseReports, analyticsReal-time alerts, dashboards
ToolsAirflow, dbt, Spark BatchKafka, Flink, Spark Streaming

Popular Pipeline Tools

Extraction Tools

Fivetran

Managed ELT, 150+ connectors, automated schema changes

Best for: No-code data ingestion
Airbyte

Open-source, customizable connectors, self-hosted or cloud

Best for: Custom integrations, cost control
dlt (data load tool)

Lightweight, open-source, Python-first EL with automatic schema inference

Best for: Python developers who want minimal configuration
AWS Glue

Fully managed ETL service, serverless Spark jobs, built-in crawlers, Data Catalog integration

Best for: AWS-native stacks, zero-infrastructure ETL, large-scale batch processing

Transformation Tools

dbt (Data Build Tool)

SQL-based, version control, testing, documentation

Best for: Analytics engineers, ELT workflows
Apache Spark

Distributed processing, batch and streaming, Python/Scala

Best for: Large-scale data processing
Pandas/Polars

Python libraries for data manipulation

Best for: Small to medium datasets, custom logic
AWS Glue

Fully managed ETL service, serverless Spark jobs, built-in crawlers, Data Catalog integration

Best for: AWS-native stacks, zero-infrastructure ETL, large-scale batch processing

Orchestration Tools

Apache Airflow

Python-based DAGs, rich ecosystem, highly flexible

Best for: Complex workflows, Python users
Prefect

Modern alternative to Airflow, better observability

Best for: Developer experience, dynamic workflows
Dagster

Asset-based, data-aware, great testing

Best for: Data quality focus, testing
Mage

Modern orchestration with great UI, notebook-style development, fast adoption

Best for: Teams wanting developer-friendly experience

Streaming Tools

Apache Kafka

Distributed event streaming, high throughput

Best for: Event-driven architectures
Apache Flink

Stream processing framework, exactly-once semantics

Best for: Complex event processing
AWS Kinesis

Managed streaming service on AWS

Best for: AWS-native applications

Orchestration: Coordinating the Pipeline

Orchestration tools schedule, monitor, and manage dependencies between pipeline tasks. They ensure tasks run in the right order, handle failures, and provide visibility.

Key Concepts

DAG (Directed Acyclic Graph)

Tasks connected by dependencies, no cycles allowed

Dependencies

Task B can't start until Task A completes successfully

Retry Logic

Automatically retry failed tasks with backoff

Example: Airflow DAG

from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.providers.postgres.operators.postgres import PostgresOperator
from datetime import datetime, timedelta

# Define default arguments
default_args = {
    'owner': 'data-team',
    'retries': 3,
    'retry_delay': timedelta(minutes=5),
    'email_on_failure': True,
    'email': ['alerts@company.com']
}

# Create DAG
dag = DAG(
    'sales_analytics_pipeline',
    default_args=default_args,
    description='Daily sales data processing',
    schedule_interval='0 2 * * *',  # 2 AM daily
    start_date=datetime(2024, 1, 1),
    catchup=False
)

# Task 1: Extract from source database
extract_sales = PostgresOperator(
    task_id='extract_sales',
    postgres_conn_id='source_db',
    sql='''
        CREATE TABLE staging_sales AS
        SELECT * FROM sales
        WHERE date = CURRENT_DATE - 1
    ''',
    dag=dag
)

# Task 2: Transform data
def transform_data():
    import pandas as pd
    df = pd.read_sql("SELECT * FROM staging_sales", conn)

    # Clean and aggregate
    df = df.dropna()
    summary = df.groupby('customer_id').agg({
        'amount': 'sum',
        'quantity': 'sum'
    })

    summary.to_sql('transformed_sales', conn, if_exists='replace')

transform_task = PythonOperator(
    task_id='transform_sales',
    python_callable=transform_data,
    dag=dag
)

# Task 3: Load to warehouse
load_warehouse = PostgresOperator(
    task_id='load_warehouse',
    postgres_conn_id='warehouse_db',
    sql='''
        INSERT INTO sales_summary
        SELECT * FROM transformed_sales
    ''',
    dag=dag
)

# Task 4: Update metrics
def update_metrics():
    # Update dashboard metrics
    calculate_daily_kpis()
    send_slack_notification("Sales pipeline completed")

update_metrics_task = PythonOperator(
    task_id='update_metrics',
    python_callable=update_metrics,
    dag=dag
)

# Define dependencies
extract_sales >> transform_task >> load_warehouse >> update_metrics_task
Result:
Tasks run in sequence: Extract → Transform → Load → Metrics
If any task fails, it retries 3 times with 5-minute delays
Email alert sent on failure

DAG Visualization

    ┌─────────────────┐
    │ extract_sales   │
    └────────┬────────┘
             │
             ▼
    ┌────────────────┐
    │transform_sales │
    └────────┬───────┘
             │
             ▼
    ┌────────────────┐
    │ load_warehouse │
    └────────┬───────┘
             │
             ▼
    ┌────────────────┐
    │ update_metrics │
    └────────────────┘
Linear dependency chain, each task waits for previous to complete

Parallel Execution

Tasks without dependencies can run in parallel for faster execution.

    ┌─────────────┐
    │   START     │
    └──────┬──────┘
           │
    ┌──────┴──────┐
    │             │
    ▼             ▼
┌────────┐   ┌────────┐
│Extract │   │Extract │  (Run in parallel)
│ Sales  │   │Products│
└───┬────┘   └───┬────┘
    │            │
    └──────┬─────┘
           │
           ▼
    ┌──────────┐
    │  Join &  │
    │Transform │
    └─────┬────┘
          │
          ▼
    ┌──────────┐
    │   Load   │
    └──────────┘
Two extract tasks run simultaneously, then join for transformation

Pipeline Best Practices

✅ Idempotency

Running the same pipeline multiple times produces the same result. Use MERGE or UPSERT instead of INSERT. Crucial for retry logic.

✅ Incremental Processing

Only process new/changed data, not the entire dataset. Use timestamps, watermarks, or change data capture (CDC) to identify what changed.

✅ Data Quality Checks

Validate data at each stage. Check for nulls, duplicates, schema changes, and business rule violations. Fail fast on bad data.

✅ Monitoring & Alerting

Track pipeline health, execution time, data volume, and failure rates. Alert on failures, SLA breaches, and anomalies.

✅ Small, Focused Tasks

Break pipelines into small, single-purpose tasks. Easier to debug, test, and reuse. Each task should do one thing well.

✅ Version Control

Store pipeline code in Git. Use CI/CD for deployment. Track changes, enable rollbacks, and collaborate effectively.

❌ Avoid Hard-Coding

Don't hard-code dates, credentials, or configuration. Use parameters, environment variables, or config files.

❌ Don't Process Everything

Full reprocessing is expensive and slow. Use incremental loads and only process changed data whenever possible.

Data Quality Checks

Always validate data before loading to catch issues early.

Example: Great Expectations

import great_expectations as ge

# Load data
df = ge.read_csv('sales_data.csv')

# Define expectations (data quality rules)
df.expect_column_values_to_not_be_null('customer_id')
df.expect_column_values_to_be_unique('order_id')
df.expect_column_values_to_be_between('amount', min_value=0, max_value=100000)
df.expect_column_values_to_be_in_set('status', ['pending', 'completed', 'cancelled'])
df.expect_column_values_to_match_regex('email', r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')

# Validate
results = df.validate()

if not results['success']:
    # Log failures
    for result in results['results']:
        if not result['success']:
            print(f"FAILED: {result['expectation_config']['expectation_type']}")
            print(f"  Details: {result['result']}")

    # Stop pipeline
    raise ValueError("Data quality checks failed!")
else:
    print("✓ All data quality checks passed")
    # Continue to load data
Result (if checks pass):
✓ All data quality checks passed
Pipeline continues to load data
Result (if checks fail):
FAILED: expect_column_values_to_not_be_null
Details: 150 null values found in customer_id
Pipeline stops, data NOT loaded

Common Pipeline Patterns

Change Data Capture (CDC)

Track only changes (inserts, updates, deletes) in source database and replicate them.

Tools: Debezium, AWS DMS, Fivetran
Use: Real-time replication, incremental loads
Slowly Changing Dimensions (SCD)

Track historical changes in dimension tables (Type 1, 2, 3).

Type 2: Keep full history with start/end dates
Use: Customer addresses, product prices over time
Lambda Architecture

Combine batch and stream processing for completeness + low latency.

Batch: Complete, accurate (hours)
Stream: Fast, approximate (seconds)
Medallion Architecture

Bronze (raw) → Silver (cleaned) → Gold (business-ready) zones.

Popular in: Data lakehouses
Use: Progressive data refinement

Key Takeaways

  • ETL transforms before loading (traditional)
  • ELT loads raw, transforms in target (modern)
  • Batch processes data in scheduled intervals
  • Streaming processes data continuously
  • Orchestration coordinates pipeline execution
  • Idempotency enables safe retries
  • Data quality checks catch issues early
  • Incremental loads save time and cost
Remember: Start simple with batch ELT, add streaming only when needed. Use managed tools (Fivetran, dbt, Airflow) instead of building from scratch. Always validate data quality before loading.