home
diamond Go Premium
Data Engineering Path  ·  Data & AI

Data Engineering + AI: The Tools Ecosystem

Overview

The modern data engineering toolchain has exploded in complexity. This guide maps the essential tools across every layer of the stack, with an honest assessment of when to use each.


Layer 1: Data Ingestion

Tool Best For When to Avoid
Fivetran SaaS sources (Salesforce, HubSpot, Stripe), minimal engineering overhead Cost at scale, limited transformation capability
Airbyte Open-source Fivetran alternative, custom connectors, self-hosted More operational overhead than Fivetran
AWS Glue AWS-native ETL, serverless, tight S3 integration Slow startup, debugging is painful
Kafka Connect Real-time streaming ingestion from databases (CDC), Kafka ecosystem Requires Kafka expertise
Debezium CDC (Change Data Capture) from PostgreSQL, MySQL, Oracle Complex setup, needs careful Kafka sizing
Apache NiFi Complex routing, transformation, drag-and-drop UI Heavy, requires dedicated ops
# Airbyte programmatic connection setup (AI-friendly IaC)
import requests

def create_airbyte_connection(source_id: str, destination_id: str, 
                               stream_name: str, sync_mode: str = "incremental_append_dedup"):
    """Create a programmatic Airbyte connection for a new data source."""
    payload = {
        "sourceId": source_id,
        "destinationId": destination_id,
        "syncCatalog": {
            "streams": [{
                "stream": {"name": stream_name},
                "config": {
                    "syncMode": sync_mode,
                    "cursorField": ["updated_at"],
                    "primaryKey": [["id"]]
                }
            }]
        },
        "scheduleType": "cron",
        "scheduleData": {"cron": {"cronExpression": "0 6 * * *", "cronTimeZone": "UTC"}},
        "status": "active"
    }

    response = requests.post(
        "http://localhost:8000/api/v1/connections/create",
        json=payload
    )
    return response.json()

Layer 2: Storage

Object Storage

Platform Ideal Use
AWS S3 AWS ecosystem, mature, cheapest at scale
GCS GCP ecosystem, BigQuery native integration
Azure ADLS Gen2 Azure ecosystem, fine-grained ACLs

Open Table Formats (The Game-Changers)

# Delta Lake - Write, Read, Merge (ACID on object storage)
from delta.tables import DeltaTable
from pyspark.sql.functions import col

# MERGE (Upsert) - handle late-arriving/updated records
target = DeltaTable.forPath(spark, "s3://curated/customers/")
updates = spark.read.parquet("s3://raw/customer_updates/today/")

target.alias("target").merge(
    updates.alias("updates"),
    "target.customer_id = updates.customer_id"
).whenMatchedUpdate(set={
    "email": col("updates.email"),
    "updated_at": col("updates.updated_at")
}).whenNotMatchedInsert(values={
    "customer_id": col("updates.customer_id"),
    "email": col("updates.email"),
    "created_at": col("updates.created_at"),
    "updated_at": col("updates.updated_at")
}).execute()

# Time Travel - query data as of 7 days ago
historical_df = spark.read \
    .format("delta") \
    .option("timestampAsOf", "2024-01-15") \
    .load("s3://curated/customers/")

# Or by version number
df_v5 = spark.read \
    .format("delta") \
    .option("versionAsOf", 5) \
    .load("s3://curated/customers/")

# GDPR Delete (row-level)
target.delete(col("customer_id") == "user_to_erase_123")
# Apache Iceberg - Multi-engine compatible
from pyiceberg.catalog import load_catalog

catalog = load_catalog("glue", **{
    "type": "glue",
    "region_name": "us-east-1"
})

# Schema evolution - add column without rewriting data
table = catalog.load_table("ecommerce.orders")
with table.update_schema() as update:
    update.add_column("delivery_method", StringType())

# Partition evolution - change how data is partitioned
with table.update_spec() as update:
    update.remove_field("year")       # Old partition
    update.add_field("month", MonthTransform(), "order_date")  # New partition
    # Old data retains old partitioning, new data uses new spec

Layer 3: Processing

Batch Processing

# PySpark production patterns
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, when, coalesce

spark = SparkSession.builder \
    .appName("ProductionETL") \
    .config("spark.sql.adaptive.enabled", "true") \
    .config("spark.sql.adaptive.skewJoin.enabled", "true") \
    .config("spark.sql.adaptive.coalescePartitions.enabled", "true") \
    .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer") \
    .getOrCreate()

# Reading with predicate pushdown (critical for performance)
df = spark.read \
    .format("iceberg") \
    .option("pushdown-limit", "true") \
    .load("s3://data-lake/catalog.ecommerce.orders") \
    .filter(
        (col("order_date") >= "2024-01-01") &  # Partition pruning
        (col("status").isin(["completed", "shipped"]))  # Predicate pushdown
    ) \
    .select("order_id", "customer_id", "revenue", "order_date")  # Column pruning

Stream Processing

# Apache Flink for true streaming (lower latency than Spark Streaming)
from pyflink.table import StreamTableEnvironment, EnvironmentSettings

env_settings = EnvironmentSettings.new_instance().in_streaming_mode().build()
t_env = StreamTableEnvironment.create(environment_settings=env_settings)

# Define Kafka source with exactly-once semantics
t_env.execute_sql("""
    CREATE TABLE clicks (
        user_id BIGINT,
        page_url STRING,
        event_time TIMESTAMP(3),
        WATERMARK FOR event_time AS event_time - INTERVAL '10' SECOND
    ) WITH (
        'connector' = 'kafka',
        'topic' = 'page_clicks',
        'properties.bootstrap.servers' = 'kafka:9092',
        'properties.isolation.level' = 'read_committed',
        'format' = 'json',
        'scan.startup.mode' = 'latest-offset'
    )
""")

# Tumbling window aggregation with late data handling
t_env.execute_sql("""
    SELECT
        TUMBLE_START(event_time, INTERVAL '5' MINUTE) as window_start,
        page_url,
        COUNT(*) as click_count,
        COUNT(DISTINCT user_id) as unique_users
    FROM clicks
    GROUP BY TUMBLE(event_time, INTERVAL '5' MINUTE), page_url
""")

Layer 4: Transformation

dbt - The Transformation Standard

-- dbt macro: reusable transformation logic
-- macros/calculate_percentile.sql
{% macro calculate_percentile(column, percentile, partition_by=None) %}
    PERCENTILE_CONT({{ percentile }}) WITHIN GROUP (ORDER BY {{ column }})
    {% if partition_by %}
        OVER (PARTITION BY {{ partition_by }})
    {% endif %}
{% endmacro %}

-- Usage in model
SELECT 
    region,
    order_date,
    {{ calculate_percentile('revenue', 0.5, 'region') }} as median_revenue,
    {{ calculate_percentile('revenue', 0.95, 'region') }} as p95_revenue
FROM orders

Layer 5: Orchestration

Apache Airflow vs. Modern Alternatives

# Dagster - Modern alternative with better type safety and UI
from dagster import asset, AssetIn, define_asset_job

@asset(
    description="Raw orders from the transaction database",
    metadata={"source": "postgresql://orders_db"}
)
def raw_orders(context):
    """Extract raw orders from PostgreSQL."""
    df = pd.read_sql("""
        SELECT * FROM orders 
        WHERE updated_at > NOW() - INTERVAL '1 DAY'
    """, conn)
    context.log.info(f"Extracted {len(df)} orders")
    return df

@asset(
    ins={"raw_orders": AssetIn()},
    description="Cleaned orders with validated schemas"
)
def cleaned_orders(raw_orders: pd.DataFrame):
    """Validate and clean orders."""
    return raw_orders \
        .pipe(remove_test_orders) \
        .pipe(validate_revenue) \
        .pipe(standardize_dates)

@asset(
    ins={"cleaned_orders": AssetIn()},
    description="Daily revenue aggregates by region"
)  
def daily_revenue(cleaned_orders: pd.DataFrame):
    return cleaned_orders.groupby(["region", "order_date"])["revenue"].sum().reset_index()

# Dagster tracks lineage, data freshness, and materializations automatically
daily_job = define_asset_job("daily_pipeline", selection=[raw_orders, cleaned_orders, daily_revenue])

Layer 6: AI / ML Tools

Key AI Tools for Data Engineers

Tool Purpose Maturity
LangChain LLM application framework, RAG, agents Stable (v0.3+)
LlamaIndex Data indexing for LLMs, excellent for RAG Stable
Feast Feature store (open source) Stable
MLflow ML experiment tracking, model registry Mature
Great Expectations Data quality testing Mature
Monte Carlo Data observability (commercial) Mature
Pinecone Managed vector database Stable
Weaviate Open-source vector database Stable
dbt Semantic Layer Define metrics once, use everywhere New
# MLflow - Model tracking and registry
import mlflow
import mlflow.sklearn
from sklearn.ensemble import GradientBoostingClassifier

mlflow.set_experiment("churn_prediction_v3")

with mlflow.start_run(run_name="gbt_hypertuning_01"):
    # Log parameters
    mlflow.log_params({
        "n_estimators": 200,
        "max_depth": 6,
        "learning_rate": 0.1,
        "feature_set_version": "v2.3",
        "training_data_date": "2024-01-15"
    })

    # Train model
    model = GradientBoostingClassifier(n_estimators=200, max_depth=6, learning_rate=0.1)
    model.fit(X_train, y_train)

    # Log metrics
    mlflow.log_metrics({
        "accuracy": accuracy_score(y_test, model.predict(X_test)),
        "precision": precision_score(y_test, model.predict(X_test)),
        "recall": recall_score(y_test, model.predict(X_test)),
        "auc_roc": roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]),
        "psi_score": 0.08  # Stability metric
    })

    # Register model
    mlflow.sklearn.log_model(
        model, 
        "churn_model",
        registered_model_name="churn_prediction",
        signature=mlflow.models.infer_signature(X_train, model.predict(X_train))
    )

    # Transition to staging
    client = mlflow.MlflowClient()
    client.transition_model_version_stage(
        name="churn_prediction",
        version=3,
        stage="Staging"
    )

Layer 7: Governance & Catalog

# DataHub - Open source data catalog
from datahub.emitter.rest_emitter import DatahubRestEmitter
from datahub.metadata.schema_classes import (
    DatasetSnapshotClass,
    DatasetPropertiesClass,
    SchemaMetadataClass,
    SchemaFieldClass,
    SchemaFieldDataTypeClass,
    StringTypeClass
)

emitter = DatahubRestEmitter("http://datahub-gms:8080")

# Register a dataset with rich metadata
dataset_urn = "urn:li:dataset:(urn:li:dataPlatform:s3,curated/orders,PROD)"

metadata_event = MetadataChangeProposalWrapper(
    entityUrn=dataset_urn,
    aspect=DatasetPropertiesClass(
        description="Production orders table - source of truth for all revenue metrics",
        customProperties={
            "team": "data-platform",
            "sla": "available by 6am UTC",
            "update_frequency": "daily",
            "data_classification": "internal",
            "owner": "data-platform@company.com"
        },
        tags=["finance", "revenue", "critical", "sla-bound"],
        externalUrl="https://internal-wiki.company.com/data/orders"
    )
)

emitter.emit(metadata_event)

Tool Selection Decision Tree

flowchart LR
    A{What are you<br>building?}

    %% Ingestion
    A -->|Ingesting SaaS Data| B[Ingestion]
    B -->|Small team, low ops| B1(Fivetran)
    B -->|Cost-conscious, technical| B2(Airbyte)

    %% Processing
    A -->|Processing Data| C[Processing]
    C -->|Batch, SQL-native| C1(dbt + Snowflake/BigQuery)
    C -->|Batch, complex logic| C2(PySpark)
    C -->|Streaming, high throughput| C3(Kafka + Flink)
    C -->|Streaming, simpler ops| C4(Spark Structured Streaming)

    %% Orchestration
    A -->|Orchestrating Pipelines| D[Orchestration]
    D -->|Standard ETL| D1(Apache Airflow)
    D -->|Python-first, modern| D2(Dagster / Prefect)
    D -->|Simple cron jobs| D3(GitHub Actions + Lambda)

    %% AI Features
    A -->|Building AI Features| E[AI / ML]
    E -->|Feature Store| E1(Feast / Tecton)
    E -->|LLM Integration| E2(LangChain + Anthropic)
    E -->|Semantic Search| E3(Pinecone / pgvector)
    E -->|Model Tracking| E4(MLflow)

    %% Governance
    A -->|Governing Data| F[Governance]
    F -->|Catalog| F1(DataHub)
    F -->|Quality| F2(Great Expectations)
    F -->|Lineage| F3(OpenLineage)

    classDef decision fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px;
    classDef category fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px;
    classDef tool fill:#e8f5e9,stroke:#4caf50,stroke-width:1px;

    class A decision;
    class B,C,D,E,F category;
    class B1,B2,C1,C2,C3,C4,D1,D2,D3,E1,E2,E3,E4,F1,F2,F3 tool;

Summary

The data engineering toolchain is both wide and deep. The key to navigating it:

  1. Don't try to master every tool — understand the problem category, then learn the tool that fits
  2. Prefer boring technology — Airflow, Spark, S3 have solved hard problems. Use them.
  3. Evaluate AI tools critically — Many "AI-native" tools add cost without commensurate value
  4. Converge on open standards — Iceberg, OpenLineage, OpenAPI ensure you're not locked in
  5. The best tool is the one your team can operate — Operational complexity kills productivity
Find this content helpful? ☕ Buy me a coffee

Entity Details

Create New Item

help

Submit Technical Query

Have a question or run into an issue? Describe it below, upload an optional screenshot, and our engineering team will answer it!

image Attach image (optional)

Submit Feedback

build Free Developer Utility Free Tool
gavel

Privacy & Legal Disclaimer

1. Client-Side Browser Processing

All utility tools on DeepEngineerHub (including Image to PDF, Text Formatters, JSON Converters, and Encryptors) execute 100% locally within your client browser using WebAssembly and JavaScript. No uploaded images, text, or documents are transmitted, collected, or stored on remote servers.

2. Limitation of Liability ("As-Is" Provision)

Tools and services are provided free of charge for convenience and educational purposes "as-is" without warranties of any kind. DeepEngineerHub shall not be held liable for any data loss, formatting inconsistencies, or indirect damages resulting from tool usage.

3. Open Source & Third-Party Software

Certain utilities utilize open-source client libraries (such as jsPDF, Mermaid.js, Pyodide) licensed under MIT, Apache, or BSD open licenses. All intellectual property remains with their respective copyright holders.