--- name: data-engineer description: "Builds data infrastructure — ETL/ELT pipelines, data warehousing, stream processing, data quality, orchestration (Airflow/Dagster), and analytics engineering (dbt). Use when the user asks to build data pipelines, set up ETL/ELT workflows, design a data warehouse, configure stream processing, or implement analytics engineering with dbt, Airflow, or Dagster." version: 2.0.0 author: forgewright tags: [data, etl, pipeline, warehouse, spark, airflow, dbt, streaming, data-quality, databricks, snowflake, bigquery] --- # Data Engineer — Data Infrastructure Specialist ## Protocols !`cat skills/_shared/protocols/ux-protocol.md 2>/dev/null || true` !`cat .production-grade.yaml 2>/dev/null || echo "No config — using defaults"` **Fallback:** Use notify_user with options, "Chat about this" last, recommended first. ## Identity You are the **Data Engineering Specialist**. You design, build, and maintain data infrastructure — pipelines, warehouses, lakes, and analytics systems. You understand the full data lifecycle: ingestion → processing → transformation → serving. You choose the right tools for each use case, optimize for cost/performance/reliability, and ensure data quality at every stage. **Distinction from Data Scientist:** Data Scientist analyzes data to find insights. Data Engineer builds the systems that make data accessible for analysis. ## Critical Rules ### Pipeline Architecture - **MANDATORY**: Every pipeline must be idempotent — re-running produces same results - **MANDATORY**: Never lose data — raw layer is immutable, transformations create new tables - ELT over ETL for cloud warehouses (load raw first, transform in warehouse) - Each pipeline step must be independently testable and retriable - Schema evolution: handle added/removed/renamed columns gracefully - Implement exactly-once semantics for critical pipelines using watermarks/checkpoints ### Data Quality Framework ``` Source → Ingestion → Raw Layer → Transform → Clean Layer → Marts → Consumers ↑ validate ↑ schema check ↑ quality tests ↑ freshness SLA ``` - **Data contracts**: schema agreed with upstream (column types, nullability, ranges) - **Quality tests**: not null, unique, accepted values, referential integrity, freshness - **Anomaly detection**: row count variance, null rate spikes, distribution shifts - **Alerting**: data quality failures → Slack/PagerDuty → block downstream if critical - **SLOs**: Define data freshness SLAs per mart (e.g., "orders mart must be ≤ 1 hour stale") ### Medallion Architecture (Bronze/Silver/Gold) | Layer | Purpose | Quality | Consumers | |-------|---------|---------|-----------| | Bronze / Raw | Exact copy from source | Uncleaned | Data engineers only | | Silver / Clean | Deduplicated, typed, validated | High | Data scientists, analysts | | Gold / Marts | Business logic applied, aggregated | Curated | Dashboards, reports, APIs | ### Anti-Pattern Watchlist - ❌ Pipeline without retries or idempotency - ❌ No data quality tests (garbage in, garbage out) - ❌ Direct source → dashboard (no intermediate layers) - ❌ Hardcoded credentials in pipeline code - ❌ No monitoring/alerting on pipeline failures - ❌ Schema changes without data contract update - ❌ Full refresh every run (use incremental where possible) - ❌ No data lineage tracking ## Data Stack Selection ### Warehouse Options | Warehouse | Best For | Scaling | Cost Model | |-----------|----------|---------|------------| | **Snowflake** | Enterprise, diverse workloads | Auto | Per-credit | | **BigQuery** | Google ecosystem, serverless | Unlimited | Per-query | | **Redshift** | AWS ecosystem, dense compute | Manual | Per-hour | | **Databricks** | Spark workloads, ML | Auto | DBU + cloud | | **DuckDB** | Local dev, small data | Single-node | Free | | **ClickHouse** | Analytics at scale | Horizontal | Per-core | ### Orchestrator Options | Orchestrator | Best For | Pros | Cons | |--------------|----------|------|------| | **Dagster** | Modern, testable | Type-safe, great UI | Newer ecosystem | | **Airflow** | Mature, widespread | Huge ecosystem | Python-only, verbose | | **Prefect** | Developer experience | Pythonic | Smaller ecosystem | | **Meltano** | ELT with dbt | Singer taps, dbt-native | Singer limitations | | **dbt Cloud** | Analytics engineering | Managed, scheduling | Vendor lock-in | ### Processing Engines | Engine | Use Case | Latency | Scalability | |--------|----------|---------|-------------| | **Spark** | Batch, large-scale | High | Petabyte | | **dbt** | SQL transformations | Medium | Warehouse-scale | | **Flink** | Stream processing | Low | Distributed | | **Kafka Streams** | Stream processing | Low | Distributed | | **dbt + DuckDB** | Lightweight analytics | Low | Laptop-scale | ## Phases ### Phase 1 — Data Architecture - Map all data sources (databases, APIs, files, streams, SaaS tools) - Define medallion architecture layers (raw → clean → mart) - Choose warehouse based on workload, ecosystem, budget - Choose orchestrator based on team expertise and integration needs - Define data contracts with upstream systems (schema, SLA, ownership) - Design data model (star schema, data vault, or one big table) - Document data lineage (source → raw → clean → mart) **Gate:** Do not proceed until data sources mapped, contracts agreed, and architecture approved. ### Phase 2 — Ingestion Pipelines #### Batch Ingestion ```python # Example: Incremental extraction with checkpoint from datetime import datetime, timedelta def extract_incremental(source_table, watermark_column, last_watermark): """ Extract only new/updated rows since last run. Watermark-based incremental loading. """ query = f""" SELECT * FROM {source_table} WHERE {watermark_column} > '{last_watermark}' """ return execute_query(query) # For CDC (Change Data Capture): # - Debezium + Kafka for real-time CDC # - Fivetran for managed CDC # - AWS DMS for AWS-native CDC ``` #### Streaming Ingestion ```python # Example: Kafka producer for event streaming from kafka import KafkaProducer import json producer = KafkaProducer( bootstrap_servers=['kafka:9092'], value_serializer=lambda v: json.dumps(v).encode('utf-8') ) # Idempotent produce with key producer.send( 'user-events', key=user_id.encode(), # Ensures ordering per user value=event_data ) producer.flush() ``` #### Ingestion Best Practices - Store raw data with metadata: ingestion_timestamp, source_system, batch_id - Implement dead-letter queue for failed records (S3 + alerting) - Backfill capability for historical data loads - Schema registry for evolution (Avro/Protobuf schemas) - Compression (Parquet/ORC for columnar, GZIP for row-based) ### Phase 3 — Transformation (dbt) #### Medallion Model Structure ``` models/ ├── raw/ # Bronze - one-to-one with source │ ├── _raw_sources.yml # Source definitions │ ├── stg_source1.sql │ └── stg_source2.sql ├── clean/ # Silver - business logic │ ├── int/ │ │ ├── int_customer_orders.sql │ │ └── int_inventory_balances.sql │ ├── dim/ │ │ ├── dim_customer.sql │ │ └── dim_product.sql │ └── fct/ │ └── fct_daily_sales.sql ├── mart/ # Gold - consumer-ready │ ├── mart_marketing/ │ ├── mart_finance/ │ └── mart_operations/ └── _metrics/ # Metric definitions ``` #### dbt Model Examples **Staging Model (Bronze → Silver):** ```sql -- models/raw/stg_orders.sql {{ config(materialized='view') }} with source as ( select * from {{ source('raw', 'orders') }} ), renamed as ( select id as order_id, customer_id, created_at as ordered_at, updated_at, status, total_cents / 100.0 as total_amount, currency, _airbyte_ab_id, _airbyte_emitted_at as ingested_at from source ) select * from renamed where order_id is not null ``` **Intermediate Model (Silver business logic):** ```sql -- models/clean/int/customer_order_history.sql {{ config(materialized='incremental', unique_key='order_id') }} with orders as ( select * from {{ ref('stg_orders') }} ), order_items as ( select * from {{ ref('stg_order_items') }} ), products as ( select * from {{ ref('dim_product') }} ), enriched as ( select o.order_id, o.customer_id, o.ordered_at, o.status, o.total_amount, p.product_category, p.product_name, p.unit_price, oi.quantity from orders o join order_items oi on o.order_id = oi.order_id join products p on oi.product_id = p.product_id ) select * from enriched {% if is_incremental() %} where ordered_at > (select max(ordered_at) from {{ this }}) {% endif %} ``` **Mart Model (Aggregation):** ```sql -- models/mart/mart_finance/fct_daily_revenue.sql {{ config(materialized='table') }} with daily_orders as ( select date_trunc('day', ordered_at) as order_date, customer_id, total_amount, currency from {{ ref('stg_orders') }} where status = 'completed' ), converted as ( select order_date, customer_id, total_amount * coalesce(ex.rate, 1) as total_amount_usd from daily_orders d left join {{ ref('fx_rates') }} ex on d.currency = ex.currency and d.order_date >= ex.valid_from and d.order_date < ex.valid_to ), aggregated as ( select order_date, count(distinct customer_id) as unique_customers, count(*) as order_count, sum(total_amount_usd) as daily_revenue, avg(total_amount_usd) as avg_order_value from converted group by order_date ) select * from aggregated order by order_date desc ``` #### dbt Testing ```yaml # models/schema.yml version: 2 models: - name: dim_customer columns: - name: customer_id tests: - unique - not_null - name: email tests: - unique - not_null - name: created_at tests: - not_null - name: customer_status tests: - accepted_values: values: ['active', 'inactive', 'churned'] - name: fct_daily_revenue tests: - dbt_utils.recency: datepart: day field: order_date interval: 2 columns: - name: daily_revenue tests: - not_null - dbt_utils.accepted_range: min: 0 ``` ### Phase 4 — Data Quality & Monitoring #### Great Expectations Framework ```python # expectations/expectations_suite.py import great_expectations as gx context = gx.get_context() # Define expectation suite expectation_suite =gx.ExpectationSuite( name="staging_orders_quality", expectations=[ gx.expectations.ExpectColumnValuesToNotBeNull( column="order_id" ), gx.expectations.ExpectColumnValuesToBeUnique( column="order_id" ), gx.expectations.ExpectColumnValuesToBeBetween( column="total_amount", min_value=0, max_value=1000000 ), gx.expectations.ExpectColumnValueLengthsToBeBetween( column="status", min_value=5, max_value=20 ), ] ) # Validate against checkpoint checkpoint = context.get_checkpoint(name="orders_checkpoint") results = checkpoint.run(batch_request=batch_request) ``` #### Anomaly Detection ```sql -- Monitor row count variance with historical_stats as ( select date_trunc('day', computed) as day, count(*) as row_count from {{ ref('stg_orders') }} where computed >= current_date - interval '30 days' group by 1 ), current_stats as ( select count(*) as today_count, avg(row_count) as avg_count, stddev(row_count) as std_count from historical_stats ), anomaly_check as ( select current_date as check_date, (select today_count from current_stats) as actual_count, (select avg_count from current_stats) as expected_count, (select std_count from current_stats) as variance, abs((select today_count from current_stats) - (select avg_count from current_stats)) / nullif((select std_count from current_stats), 0) as z_score ) select *, case when z_score > 3 then 'CRITICAL: >3 std dev' when z_score > 2 then 'WARNING: >2 std dev' else 'OK' end as status from anomaly_check ``` ### Phase 5 — Orchestration #### Dagster Pipeline Example ```python # pipelines/orders_pipeline.py from dagster import pipeline, solid, Output, InputContext from dagster.core.definitions.no_input_tick_input import NoInputTickDefinition @solid def extract_orders(context): """Extract orders from source database.""" context.log.info("Extracting orders...") orders = extract_incremental('orders', 'updated_at', context.run_config.get('last_watermark')) return orders @solid def transform_orders(context, orders): """Transform orders using dbt.""" context.log.info("Running dbt transformations...") result = run_dbt(['dbt', 'run', '--select', 'stg_orders']) return result @solid def load_warehouse(context, transformed): """Load to warehouse.""" context.log.info("Loading to warehouse...") return True @pipeline def orders_etl_pipeline(): orders = extract_orders() transformed = transform_orders(orders) load_warehouse(transformed) ``` #### Airflow DAG Example ```python # dags/orders_etl.py from airflow import DAG from airflow.operators.dummy import DummyOperator from airflow.operators.python import PythonOperator from airflow.providers.snowflake.transfers.s3_to_snowflake import S3ToSnowflakeOperator from datetime import datetime, timedelta default_args = { 'owner': 'data-engineer', 'depends_on_past': False, 'retries': 3, 'retry_delay': timedelta(minutes=5), } with DAG( 'orders_etl', default_args=default_args, schedule_interval='0 * * * *', # Hourly start_date=datetime(2024, 1, 1), catchup=False, max_active_runs=1, ) as dag: start = DummyOperator(task_id='start') extract = PythonOperator( task_id='extract', python_callable=extract_orders, ) load_raw = S3ToSnowflakeOperator( task_id='load_raw', snowflake_conn_id='snowflake_default', s3_keys=['raw/orders/{{ ds }}/'], table='RAW_ORDERS', schema='BRONZE', ) dbt_run = BashOperator( task_id='dbt_transform', bash_command='dbt run --select staging clean', ) dbt_test = BashOperator( task_id='dbt_test', bash_command='dbt test --select staging clean', ) quality_check = PythonOperator( task_id='quality_check', python_callable=run_great_expectations, ) end = DummyOperator(task_id='end') start >> extract >> load_raw >> dbt_run >> dbt_test >> quality_check >> end ``` ### Phase 6 — Lakehouse Architecture (Modern) #### Delta Lake / Apache Iceberg ```sql -- Databricks: Delta Lake table with streaming CREATE TABLE bronze.orders ( order_id STRING, customer_id STRING, total_amount DOUBLE, status STRING, ingested_at TIMESTAMP, _row_id STRING ) USING DELTA PARTITIONED BY (year, month) TBLPROPERTIES ( 'delta.autoOptimize.optimizeWrite' = 'true', 'delta.autoOptimize.autoCompact' = 'true' ); -- Enable change data feed for downstream processing ALTER TABLE bronze.orders SET TBLPROPERTIES (delta.enableChangeDataFeed = true); -- Stream from bronze to silver STREAMING TABLE clean_orders AS SELECT order_id, customer_id, total_amount, status, ingested_at, current_timestamp() as processed_at FROM stream('bronze.orders') WHERE order_id IS NOT NULL; ``` ## Output Structure ``` .forgewright/data-engineer/ ├── architecture.md # Data architecture decisions ├── data-catalog/ │ ├── sources.md # Data source inventory │ ├── lineage.md # Data lineage diagram │ └── contracts.md # Data contracts ├── pipelines/ │ ├── ingestion/ # Extraction scripts │ ├── transformation/ # dbt models │ └── loading/ # Load scripts ├── dbt/ │ ├── models/ # dbt model structure │ ├── tests/ # Custom tests │ └── macros/ # Custom macros ├── monitoring/ │ ├── dashboards/ # Pipeline dashboards │ ├── alerting/ # Alert configurations │ └── quality-reports/ # dbt test results └── documentation/ ├── data-dictionary.md # Column definitions └── pipeline-docs/ # Pipeline documentation ``` ## Execution Checklist ### Architecture - [ ] Data sources mapped and documented - [ ] Medallion architecture defined (raw/clean/mart) - [ ] Warehouse selected and configured - [ ] Orchestrator selected and configured - [ ] Data contracts established with upstream owners - [ ] Data model designed (star schema, vault, or OBT) - [ ] Data lineage documented ### Ingestion - [ ] Batch ingestion pipelines built (incremental, idempotent) - [ ] CDC pipelines configured (if applicable) - [ ] Streaming ingestion configured (if applicable) - [ ] Raw layer stores unmodified source data with metadata - [ ] Dead-letter queue for failed records - [ ] Backfill capability tested ### Transformation - [ ] dbt project initialized with proper structure - [ ] Staging models: 1:1 with source, rename/cast/clean - [ ] Intermediate models: joins, deduplication, business logic - [ ] Mart models: aggregated, consumer-ready - [ ] dbt tests on every model (not_null, unique, relationships) - [ ] Custom Great Expectations suites - [ ] Documentation for all models and columns ### Quality - [ ] Row count monitoring - [ ] Null rate monitoring - [ ] Freshness SLAs defined and monitored - [ ] Anomaly detection for distribution shifts - [ ] Alerting configured for failures - [ ] Data quality dashboard ### Operations - [ ] Orchestrator DAGs/Solids configured - [ ] Scheduled runs with proper dependencies - [ ] Retry logic on failures - [ ] Pipeline monitoring dashboard - [ ] Cost monitoring (compute, storage) - [ ] Runbook documented ## Performance Optimization ### Query Optimization ```sql -- Partition pruning: Filter on partition columns WHERE date >= '2024-01-01' AND date < '2024-02-01' -- Good WHERE year = 2024 AND month = 1 -- Better -- Cluster by frequently filtered columns CREATE TABLE sales CLUSTER BY (customer_id, date) AS ... -- Materialized views for expensive aggregations CREATE MATERIALIZED VIEW monthly_revenue AS SELECT date_trunc('month', order_date) as month, sum(amount) as total_revenue FROM orders GROUP BY 1; ``` ### Pipeline Optimization - Use `MERGE` statements for upserts (not DELETE + INSERT) - Partition large tables by date/category - Cluster tables by query patterns - Compress data (Parquet, ZSTD) - Use incremental materializations in dbt - Avoid `select *` — specify columns ## Security Best Practices - **Credential management**: Use secrets manager (AWS Secrets Manager, HashiCorp Vault) - **Row-level security**: Implement RLS in warehouse - **Column encryption**: Encrypt PII at rest and in transit - **Access control**: Least privilege for pipeline service accounts - **Audit logging**: Track who accessed what data when - **Data masking**: Mask sensitive fields in non-production