Skip to content

Data Pipelines

Data pipelines move and transform data from source to destination. This sheet covers architecture patterns, orchestration, monitoring, error handling, testing, and production best practices.

Table of Contents

Installation / Setup

# Common pipeline tools
pip install apache-airflow    # orchestration
pip install great-expectations  # data quality
pip install dbt-core          # transformation
pip install pandas            # single-machine
pip install pyspark           # distributed

Core Syntax

Pipeline Architecture Patterns

Pattern Description When to Use
Batch Process data in scheduled batches Daily/hourly reporting
Streaming Process data in real-time Low-latency requirements
Lambda Batch + streaming layers Flexibility + real-time
Kappa Streaming only Simplicity; event-sourced
Medallion Bronze/Silver/Gold layers Lakehouse architecture
Medallion Architecture:
Bronze: Raw data (ingested)
Silver: Cleaned, validated
Gold: Aggregated, business-ready

Pipeline Components

Source → Ingestion → Storage → Processing → Serving → Destination

Components:
- Source: databases, APIs, files, streams
- Ingestion: connectors, CDC, webhooks
- Storage: data lake, warehouse, lakehouse
- Processing: batch, streaming, transformation
- Serving: APIs, dashboards, exports
- Destination: databases, files, applications

DAG (Directed Acyclic Graph)

# Pipeline as a DAG
from airflow import DAG
from airflow.operators.python import PythonOperator

with DAG('my_pipeline', schedule_interval='@daily') as dag:
    extract = PythonOperator(task_id='extract', python_callable=extract)
    transform = PythonOperator(task_id='transform', python_callable=transform)
    load = PythonOperator(task_id='load', python_callable=load)
    validate = PythonOperator(task_id='validate', python_callable=validate)

    extract >> transform >> load >> validate

Common Operations

Orchestration

# Airflow DAG
from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime

def extract(**context):
    # Extract data
    pass

def transform(**context):
    # Transform data
    pass

def load(**context):
    # Load data
    pass

with DAG(
    'etl_pipeline',
    start_date=datetime(2026, 1, 1),
    schedule_interval='@daily',
    catchup=False,
) as dag:
    t1 = PythonOperator(task_id='extract', python_callable=extract)
    t2 = PythonOperator(task_id='transform', python_callable=transform)
    t3 = PythonOperator(task_id='load', python_callable=load)

    t1 >> t2 >> t3

Monitoring

import time
import logging
from datetime import datetime

class PipelineMonitor:
    def __init__(self, pipeline_name):
        self.pipeline_name = pipeline_name
        self.start_time = None
        self.metrics = {}

    def start(self):
        self.start_time = time.time()
        logging.info(f"Pipeline {self.pipeline_name} started")

    def record_metric(self, name, value):
        self.metrics[name] = value

    def finish(self, success=True):
        duration = time.time() - self.start_time
        self.metrics['duration_seconds'] = duration
        self.metrics['success'] = success
        self.metrics['end_time'] = datetime.now().isoformat()

        logging.info(f"Pipeline {self.pipeline_name} finished: {self.metrics}")

        # Send to monitoring system
        self.send_to_monitoring()

    def send_to_monitoring(self):
        # Send metrics to Prometheus, Datadog, etc.
        pass

Error Handling

from airflow.models import Variable
from airflow.utils.email import send_email

def handle_failure(context):
    """Handle pipeline failure."""
    # Log error
    logging.error(f"Pipeline failed: {context}")

    # Send alert
    send_email(
        to=['team@example.com'],
        subject=f"Pipeline failed: {context['dag'].dag_id}",
        html_content=str(context)
    )

# Use in DAG
default_args = {
    'on_failure_callback': handle_failure,
    'retries': 3,
    'retry_delay': timedelta(minutes=5),
}

Testing

import pytest
import pandas as pd

def test_extract():
    """Test extraction."""
    df = extract_data()
    assert len(df) > 0
    assert 'id' in df.columns

def test_transform():
    """Test transformation."""
    df = extract_data()
    result = transform_data(df)
    assert result.isnull().sum().sum() == 0
    assert len(result) > 0

def test_load():
    """Test loading."""
    df = transform_data(extract_data())
    count = load_data(df)
    assert count == len(df)

# Integration test
def test_pipeline():
    """Test full pipeline."""
    df = run_pipeline()
    assert len(df) > 0
    assert validate_data(df)

Practical Examples

Example 1 — Batch pipeline

from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime, timedelta

default_args = {
    'owner': 'data-team',
    'retries': 2,
    'retry_delay': timedelta(minutes=5),
    'on_failure_callback': handle_failure,
}

def extract_orders(**context):
    import pandas as pd
    df = pd.read_sql("SELECT * FROM orders WHERE date = %s", conn, params=[context['ds']])
    return df.to_parquet('/staging/orders.parquet')

def transform_orders(**context):
    import pandas as pd
    df = pd.read_parquet('/staging/orders.parquet')
    df['total'] = df['quantity'] * df['price']
    df.to_parquet('/staging/orders_transformed.parquet')

def load_orders(**context):
    import pandas as pd
    df = pd.read_parquet('/staging/orders_transformed.parquet')
    df.to_sql('orders_daily', conn, if_exists='append', index=False)

with DAG('daily_orders', default_args=default_args, schedule_interval='0 2 * * *') as dag:
    t1 = PythonOperator(task_id='extract', python_callable=extract_orders)
    t2 = PythonOperator(task_id='transform', python_callable=transform_orders)
    t3 = PythonOperator(task_id='load', python_callable=load_orders)

    t1 >> t2 >> t3

Example 2 — Multi-source pipeline

def extract_from_source(source):
    def extract():
        if source == 'api':
            return extract_from_api()
        elif source == 'database':
            return extract_from_db()
        elif source == 'file':
            return extract_from_file()
    return extract

sources = ['api', 'database', 'file']

with DAG('multi_source', default_args=default_args) as dag:
    extracts = [
        PythonOperator(task_id=f'extract_{s}', python_callable=extract_from_source(s))
        for s in sources
    ]

    merge = PythonOperator(task_id='merge', python_callable=merge_data)
    load = PythonOperator(task_id='load', python_callable=load_data)

    extracts >> merge >> load

Example 3 — Data quality pipeline

import great_expectations as gx

def validate_data():
    context = gx.get_context()
    batch_request = context.sources.pandas_default.read_csv("data.csv")

    suite = context.add_expectation_suite("my_suite")
    suite.add_expectation(
        gx.expectations.ExpectColumnValuesToNotBeNull(column="id")
    )
    suite.add_expectation(
        gx.expectations.ExpectColumnValuesToBeBetween(
            column="amount", min_value=0, max_value=10000
        )
    )

    result = context.run_checkpoint("my_checkpoint")
    assert result.success

Common Patterns

Pattern: retry with exponential backoff

import random

def retry_with_backoff(func, max_retries=3, base_delay=1):
    for attempt in range(max_retries):
        try:
            return func()
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
            time.sleep(delay)

Pattern: circuit breaker

class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=60):
        self.failure_count = 0
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.last_failure_time = None

    def call(self, func):
        if self.is_open():
            raise Exception("Circuit breaker is open")
        try:
            result = func()
            self.reset()
            return result
        except Exception as e:
            self.record_failure()
            raise

    def is_open(self):
        if self.last_failure_time is None:
            return False
        return (time.time() - self.last_failure_time) < self.recovery_timeout

    def record_failure(self):
        self.failure_count += 1
        self.last_failure_time = time.time()

    def reset(self):
        self.failure_count = 0
        self.last_failure_time = None

Pattern: dead letter queue

def process_with_dlq(messages):
    dlq = []

    for message in messages:
        try:
            process(message)
        except Exception as e:
            dlq.append({'message': message, 'error': str(e)})

    # Store failed messages for reprocessing
    if dlq:
        store_dlq(dlq)

Best Practices

  • Idempotent tasks — safe to rerun without side effects.
  • Atomic transactions — all or nothing for data loads.
  • Monitoring and alerting — know when things fail.
  • Data lineage — track where data comes from and goes.
  • Testing — unit tests, integration tests, data quality tests.
  • Documentation — document schemas, dependencies, and runbooks.
  • Version control — track pipeline code changes.
  • Separation of concerns — extract, transform, load as separate steps.

Common Mistakes

Mistake Fix
No error handling Add try/except and alerting
No monitoring Track metrics and alert
No testing Add unit and integration tests
Hardcoding credentials Use secrets management
No lineage tracking Add metadata and audit columns
Tight coupling Make components independent
No rollback strategy Implement transactions and backups

Troubleshooting

Symptom Cause / Fix
Pipeline fails silently Add error handling and alerting
Slow performance Profile and optimize bottlenecks
Data quality issues Add validation; check sources
Duplicates Use idempotent loads; upsert
Missing data Check source connectivity; check logs
Memory errors Process in batches; increase resources

Official Documentation

Last Updated

2026-09-06 — Initial version.