Claude Skill

sota-data-engineering

State-of-the-art data engineering rules (2026) for building and auditing data pipelines and analytics infrastructure. Covers architecture and modeling (ELT, lakehouse vs warehouse, dimensional models, medallion layering), pipeline and orchestration discipline (idempotency, increm

LLM Mart · 0 points · 6 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download martinholovsky-sota-skills-skills_sota-data-engineering-582d6f9.zip · 32 KB
Part of martinholovsky/sota-skills — 39 skills

Install

skills CLI npx skills add https://github.com/martinholovsky/SOTA-skills/tree/main/skills/sota-data-engineering
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install martinholovsky-sota-skills@llmmart
Git git clone https://github.com/martinholovsky/SOTA-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole martinholovsky/sota-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

SOTA Data Engineering

Expert rules for analytical data systems: pipelines, streaming, warehousing, lakehouse storage, data quality, and operations. OLTP schema/query/index craft is owned by sota-databases — reference it, do not duplicate it. Outbox and event-driven service patterns live in sota-architecture; backpressure mechanics in sota-async-concurrency; PII handling in sota-privacy-compliance (or sota-code-security if absent).

Two modes. Pick by intent, then load only the rules/ files the task needs.

BUILD mode

Use when designing or implementing pipelines, models, streaming jobs, or storage layouts.

  1. Size the problem first. Read rules/01-architecture-and-modeling.md before choosing tools. Most "big data" is small data; DuckDB/Polars on one node before a distributed engine. ELT into a warehouse/lakehouse is the default shape, not a decision to revisit per pipeline.
  2. Idempotency is the prime directive. Every pipeline you write must be safe to rerun for any interval at any time. No blind appends. Design the write strategy (partition overwrite / merge key / insert-overwrite) before the transform logic (rules/02-pipelines-and-orchestration.md).
  3. Batch unless a consumer needs sub-minute data. Justify streaming in writing before building it (rules/03-streaming-and-cdc.md).
  4. Quality checks ship with the pipeline, not after. Every new model gets freshness, volume, uniqueness, and not-null checks tiered block/warn (rules/04-data-quality-and-contracts.md).
  5. Decide the physical layout when you create the table. Partitioning, clustering/sort, file sizing, and the maintenance job are part of the table's definition (rules/05-storage-and-performance.md).
  6. Ship with operability. Dev/prod isolation, write-audit-publish for risky changes, freshness alerting, a runbook entry (rules/06-operations-and-governance.md).

AUDIT mode

Use when reviewing an existing pipeline repo, dbt project, streaming topology, or warehouse.

Procedure:

  1. Inventory: orchestrator + scheduler config, transformation tool (dbt or other), storage/table formats, streaming components, quality tooling, environments. Read the actual DAGs/models — never audit from README claims.
  2. Load the rules files matching what exists (no Kafka → skip 03).
  3. Verify every finding against real code/config/SQL. Confirm a non-idempotent write by reading the write statement, not by inferring from naming.
  4. Report findings in the format below, ordered by severity.

Severity conventions:

  • CRITICAL — data corruption or silent wrongness: non-idempotent writes that double-count on retry, reruns that duplicate or lose data, CDC deletes not applied, PII landing in unprotected zones, prod credentials in dev.
  • HIGH — likely incident or unbounded cost: no failure alerting on business-critical pipelines, unbounded retries, full-table rescans of large sources each run, no backfill path, schema changes that break consumers.
  • MEDIUM — erodes trust/efficiency: missing quality checks, SELECT * staging, small-files accumulation, no documentation/lineage, warn-tier checks failing for weeks.
  • LOW — hygiene: naming inconsistency, missing column descriptions, suboptimal compression.

Finding format:

[SEVERITY] <one-line title>
Where: <file:line / model / DAG / topic>
Evidence: <the actual code/config/SQL that proves it>
Impact: <what goes wrong, when>
Fix: <concrete change, smallest safe diff>

Rules index

File Read this when...
rules/01-architecture-and-modeling.md Choosing engines/architecture (warehouse vs lakehouse vs DuckDB), designing layers (staging/core/mart), dimensional modeling, SCDs, One Big Table, semantic layers, evaluating a data-mesh pitch.
rules/02-pipelines-and-orchestration.md Writing or reviewing any batch pipeline: idempotency, incremental loads, watermarks, late data, backfills, Airflow/orchestrator DAG design, dbt project discipline, scheduling strategy.
rules/03-streaming-and-cdc.md Anything Kafka/Flink/CDC: deciding streaming vs micro-batch, partition keys, consumer groups, offsets, exactly-once claims, schema registry, Debezium, tombstones, DLQs, windowing.
rules/04-data-quality-and-contracts.md Defining data contracts, adding expectation tests, tiering checks block vs warn, drift/anomaly detection, lineage, responding to a data incident, testing transforms in CI.
rules/05-storage-and-performance.md Creating tables, Parquet tuning, partitioning vs clustering, small-files/compaction, Iceberg/Delta features and maintenance, compression, reducing scan cost / warehouse spend.
rules/06-operations-and-governance.md Environments and deployment of pipeline changes, write-audit-publish, blue-green tables, access control, GDPR deletion in lakehouses, pipeline observability, runbooks.

Top 10 non-negotiables

  1. Every pipeline is idempotent. Rerunning any task for any interval produces the same result. Overwrite partitions or MERGE on keys; a blind INSERT INTO ... SELECT in a scheduled job is a CRITICAL finding.
  2. No silent failure. Business-critical pipelines have failure AND freshness alerts routed to an owner. A pipeline that fails quietly is worse than one that doesn't exist — people keep trusting its output.
  3. Right-size the engine. Under ~100 GB working set, single-node DuckDB/Polars beats a cluster on cost, speed, and ops. Spark/distributed engines need a stated reason (data volume, existing platform, ML scale).
  4. Incremental by watermark, never by NOW() arithmetic in the task. Process data by the orchestrator-supplied logical interval; reruns and backfills must produce identical results regardless of wall-clock time.
  5. Schema changes are backward-compatible or coordinated. Add columns freely; never rename, retype, or drop in place. Producers that break consumers without a contract bump are HIGH findings.
  6. Quality checks are tiered. Block (fail the pipeline, stop downstream) for uniqueness/null/contract violations on critical models; warn (alert, continue) for distribution drift. Everything-blocks and nothing-blocks are both failure modes.
  7. Streaming requires a written justification. Name the consumer that needs sub-minute latency. Hourly micro-batch covers most "real-time" requests at a tenth of the operational cost.
  8. Exactly-once is end-to-end or it's a lie. Kafka transactions cover Kafka; your sink makes it true via idempotent writes (merge keys, deterministic IDs). Audit the sink, not the producer config.
  9. No SELECT * across layer boundaries. Staging models enumerate, rename, and type columns. Upstream schema drift must break loudly in your staging layer, not silently in a dashboard.
  10. Tables get maintenance from day one. Compaction, snapshot/manifest cleanup, and retention jobs are part of creating an Iceberg/Delta table. A lakehouse without maintenance jobs is a slow-motion outage.
Files (sota-skills)
  • rules
    • 01-architecture-and-modeling.md 10.2 KB
      # 01 — Architecture & Modeling
      
      Decisions in this file are made once per platform, not once per pipeline.
      When auditing, mismatches between the platform's stated architecture and what
      the code actually does are findings in themselves.
      
      ## Size the problem honestly
      
      - **Default to single-node engines below ~100 GB working set.** DuckDB (1.5.x
        as of mid-2026) or Polars on one machine outperforms a Spark cluster on
        cost, latency, and operational burden for the overwhelming majority of
        analytical workloads. "We might grow" is not a reason; migrating a
        well-modeled SQL project later is cheaper than running Spark for 5 GB now.
      - **Reach for distributed engines (Spark, Trino, warehouse MPP) when:** the
        working set genuinely exceeds single-node memory+disk economics (TBs per
        query), you need concurrent heavy users on shared compute, or the
        organization already operates the platform and marginal cost is near zero.
      - **AUDIT:** A Spark/EMR/Dataproc cluster processing < 50 GB/day with no
        growth trajectory is a MEDIUM cost finding. Quantify: rows/bytes per run vs
        cluster size.
      
      ```python
      # BAD: 8-node Spark cluster for a 3 GB daily file
      df = spark.read.parquet("s3://bucket/daily/")  # 3 GB
      df.groupBy("region").agg(...).write.parquet(...)
      
      # GOOD: same job, one process, no cluster
      import duckdb
      duckdb.sql("""
        COPY (SELECT region, sum(amount) FROM 's3://bucket/daily/*.parquet'
              GROUP BY region)
        TO 's3://bucket/marts/region_daily.parquet'
      """)
      ```
      
      ## ELT over ETL — and the exceptions
      
      - **Default: extract-load raw, transform in the warehouse/lakehouse (ELT).**
        Raw data lands unmodified; transformations are versioned SQL run where the
        data lives. This gives replayability (re-derive everything from raw),
        auditability, and lets analysts iterate without re-extraction.
      - **Land raw data immutably.** The landing zone is append-only, partitioned
        by load time, retained per policy. Never "fix" raw data — fix the
        transform and rerun.
      - **Transform before load (ETL) only when justified:** PII must be
        masked/tokenized before it touches the analytical store (see
        `sota-privacy-compliance`); the source format is pathological (mainframe
        copybooks, multi-GB XML) and pre-parsing is cheaper than in-warehouse
        parsing; volume reduction at the edge materially cuts transfer/storage cost.
      - **Never transform-in-flight as an excuse to skip raw retention.** If the
        transform is wrong, untransformed data is gone.
      
      ## Warehouse vs lakehouse
      
      Decide by ownership and access patterns, not fashion. Both converge: every
      major warehouse reads/writes Iceberg (v3 is GA on Snowflake/Databricks and
      Spark 4.0; verify your engine — Trino v3 support still lagged as of writing),
      and lakehouse engines speak SQL.
      
      - **Choose a managed warehouse (Snowflake/BigQuery/Redshift-class) when:**
        one team owns the data end-to-end, workloads are SQL-dominant, you want
        zero storage-layer ops, and per-query cost is acceptable.
      - **Choose a lakehouse (Iceberg/Delta on object storage) when:** multiple
        engines must read the same tables (Spark + Trino + warehouse + Python),
        data volume makes warehouse storage pricing punitive, you need open-format
        exit options, or ML workloads need direct file access.
      - **Hybrid is the 2026 norm:** lakehouse for bronze/silver bulk, warehouse
        (often via external Iceberg tables) for gold/serving. Avoid copying data
        between them when external-table access suffices.
      - **Table format choice:** Iceberg if multi-engine neutrality matters (widest
        catalog/engine support, REST catalog standard); Delta if Databricks is the
        center of gravity. Do not run both as peers — pick one as the canonical
        format. DuckLake (1.0+ as of mid-2026) is viable for DuckDB-centric small
        platforms but verify engine/ecosystem fit before committing.
      - **AUDIT:** Same dataset copied into both a lake and a warehouse with two
        transform stacks = HIGH (divergence is inevitable). Two table formats with
        no stated rationale = MEDIUM.
      
      ## Layered modeling (medallion / staging-core-mart)
      
      Names differ (bronze/silver/gold ≈ raw/staging/core/mart); the contract is
      what matters:
      
      - **Raw/bronze:** source-faithful, append-only, never queried by consumers.
        Schema = whatever the source sent, plus load metadata (`_loaded_at`,
        `_source_file`).
      - **Staging/silver (1:1 with source entities):** rename to house conventions,
        cast types, deduplicate, no joins, no business logic. One staging model per
        source table. This is where upstream drift breaks loudly.
      - **Core/intermediate:** business entities and processes — joins, grain
        changes, business logic, dimensional models live here.
      - **Mart/gold:** consumer-shaped, documented, contracted. Dashboards and
        reverse-ETL read only from here.
      - **Rules:** consumers never read below mart/core; no layer reads from a
        layer above itself; no model reads raw except its own staging model.
      - **AUDIT:** Dashboards querying raw/bronze = HIGH. Business logic in staging
        (joins, CASE-heavy derivations) = MEDIUM. Circular/layer-skipping refs =
        MEDIUM.
      
      ```sql
      -- BAD staging model: SELECT *, business logic, a join
      SELECT *, CASE WHEN o.status IN ('x','y') THEN 'churn' END AS churn_flag
      FROM raw.orders o JOIN raw.customers c ON o.cust = c.id;
      
      -- GOOD staging model: enumerate, rename, cast, dedupe. Nothing else.
      SELECT
        order_id::bigint            AS order_id,
        cust                        AS customer_id,
        lower(status)               AS order_status,
        created::timestamptz        AS created_at
      FROM raw.orders
      QUALIFY row_number() OVER (PARTITION BY order_id ORDER BY _loaded_at DESC) = 1
      ```
      
      ## Dimensional modeling still matters
      
      Star schemas remain the right core-layer shape for business processes:
      cheap-to-scan fact tables plus reusable conformed dimensions. Columnar
      engines did not obsolete them — they made the joins cheap.
      
      - **Facts:** one row per business event at a declared grain. Write the grain
        in the model docs ("one row per order line per day"). Mixed-grain facts are
        a MEDIUM finding. Facts carry foreign keys + numeric measures; degenerate
        dimensions (order number) are fine inline.
      - **Dimensions:** conformed and reused. One `dim_customer` shared by all
        facts, not per-mart copies that drift.
      - **Surrogate keys:** deterministic hashes of the natural key
        (`md5(source || natural_key)`) beat sequence-generated keys in
        rebuild-from-raw ELT — they're stable across full rebuilds.
      
      ### Slowly changing dimensions — pick deliberately
      
      - **Type 1 (overwrite):** default when history of the attribute doesn't
        drive analysis (fixing typos, current email). Cheapest; destroys history.
      - **Type 2 (versioned rows with `valid_from`/`valid_to`/`is_current`):** use
        when facts must join to the attribute *as it was at event time* (customer
        tier at purchase, sales territory at booking). Facts join on key + date
        range, or capture the dimension surrogate key at load time.
      - **Type 3 / hybrids:** narrow uses (single "previous value" column). Don't
        build elaborate Type 6 machinery on speculation.
      - **In ELT, prefer snapshots over hand-rolled SCD merge logic** (dbt
        snapshots or equivalent): capture source state on schedule, derive Type 2
        ranges from snapshots. Hand-written SCD2 MERGEs are a classic source of
        silent corruption — audit their handling of deletes and reruns closely.
      - **AUDIT:** Analyses that need point-in-time attributes joined against a
        Type 1 dimension = HIGH (numbers are silently wrong for any historical
        period).
      
      ## One Big Table pragmatism
      
      Wide denormalized tables (OBT) are legitimate **mart-layer** artifacts:
      build the star in core, then flatten into OBT where a BI tool or consumer
      benefits. Columnar storage makes width nearly free to store and scan-prune.
      
      - OBT as the *only* model (no dimensional core underneath) = MEDIUM: every
        new question forces re-deriving logic, and SCD handling becomes ad hoc.
      - Never maintain the same metric logic in two OBTs. Derive both from one
        core model.
      
      ## Semantic layers
      
      - Define each business metric **once** — in a semantic layer (dbt MetricFlow,
        Cube, LookML-class) if you have multiple BI/consumer surfaces, or simply in
        a single mart model if you have one. The anti-pattern is the same metric
        hand-written in five dashboards.
      - A semantic layer is justified by *consumer multiplicity*, not team size.
        One BI tool + one team → mart models are your semantic layer; don't add a
        product.
      - **AUDIT:** Grep dashboards/notebooks for re-implemented revenue/active-user
        definitions. Divergent definitions of one metric = HIGH (trust erosion is
        the most expensive data failure).
      
      ## Data mesh caution
      
      Data mesh is an **org pattern** (domain ownership, data-as-a-product,
      federated governance), not a technology purchase. Apply it only when multiple
      domain teams *already* have engineers who can own pipelines end-to-end.
      
      - A central 4-person data team "adopting mesh" is a red flag: it produces
        fragmentation without ownership.
      - The durable, steal-able ideas regardless of mesh: producer-owned data
        contracts (rules/04), domain-aligned mart ownership, self-serve platform
        tooling.
      - **AUDIT:** "Mesh" with no contracts, no per-domain owners on call for their
        data products, and one shared platform team doing all the work = the label
        is decorative; assess it as a centralized platform.
      
      ## Audit checklist
      
      - [ ] Engine size vs data size: any distributed cluster processing < 50
            GB/day without rationale?
      - [ ] Raw layer exists, is append-only/immutable, and is not queried by
            consumers or BI tools?
      - [ ] Staging models: 1:1 with sources, columns enumerated (no `SELECT *`),
            typed, deduped, no joins/business logic?
      - [ ] Layer discipline: no consumer reads below mart/core; no
            layer-skipping or circular references?
      - [ ] Each fact table has a documented, single grain?
      - [ ] Dimensions conformed (one copy) and SCD strategy explicit per
            dimension; point-in-time analyses backed by Type 2 or snapshots?
      - [ ] Surrogate keys stable under full rebuild?
      - [ ] Key metrics defined exactly once; no divergent copies in
            dashboards/marts?
      - [ ] Single canonical table format; no unjustified duplicate storage of the
            same dataset across lake and warehouse?
      - [ ] If "data mesh" is claimed: named domain owners, contracts, and
            operational responsibility actually exist?
      
    • 02-pipelines-and-orchestration.md 10.2 KB
      # 02 — Pipelines & Orchestration
      
      The prime directive: **every pipeline run is safe to repeat.** Retries,
      backfills, and "just rerun it" are how data platforms are actually operated;
      a pipeline that corrupts on rerun is broken even if its happy path is
      perfect.
      
      ## Idempotency — the prime directive
      
      A task given the same logical interval must produce the same stored result
      no matter how many times it runs, and a partial failure mid-run must not
      leave double-counted or half-written data.
      
      - **Choose the write strategy before the transform:**
        - *Partition overwrite* (delete-insert or `INSERT OVERWRITE` /
          `replace_where`): default for interval-partitioned facts. The task owns
          exactly its interval's partition(s) and replaces them wholesale.
        - *MERGE on a unique key:* for upserted entities/dimensions and late-data
          cases where the interval doesn't bound the affected rows.
        - *Full rebuild:* fine for small dimensions/marts — simplest possible
          idempotency.
        - *Blind append:* only into the immutable raw landing zone keyed by a
          unique load ID, never into modeled layers. **A scheduled `INSERT INTO ...
          SELECT` against a modeled table is a CRITICAL finding** — every retry
          duplicates rows.
      - **Writes must be atomic per run.** Table formats (Iceberg/Delta) give
        atomic commits; on plain Parquet, write to a temp prefix and swap. Never
        delete-then-insert as two separately failable steps without a transaction
        or atomic swap.
      - **Side effects too:** notifications, reverse-ETL pushes, API calls inside
        pipelines need idempotency keys, or must move to a terminal step that runs
        only after the data write commits.
      
      ```sql
      -- BAD: duplicates on every retry/rerun
      INSERT INTO fct_orders
      SELECT ... FROM stg_orders WHERE order_date = '{{ ds }}';
      
      -- GOOD: rerun-safe partition replacement (engine-equivalents: Iceberg
      -- INSERT OVERWRITE, Delta replaceWhere, BigQuery MERGE/partition decorator)
      DELETE FROM fct_orders WHERE order_date = '{{ ds }}';
      INSERT INTO fct_orders SELECT ... FROM stg_orders WHERE order_date = '{{ ds }}';
      -- (as one transaction / atomic commit)
      
      -- GOOD: keyed merge when rows aren't bounded by the interval
      MERGE INTO dim_customer t USING batch s ON t.customer_id = s.customer_id
      WHEN MATCHED THEN UPDATE SET ...
      WHEN NOT MATCHED THEN INSERT ...;
      ```
      
      ## Logical time, not wall-clock time
      
      - **The orchestrator supplies the interval; the task uses only that.**
        `WHERE created_at >= NOW() - INTERVAL '1 day'` is a HIGH finding: results
        depend on when the task ran, reruns produce different data, and backfills
        are impossible.
      - Parameterize every extraction and transform by `[interval_start,
        interval_end)`. Half-open intervals; never overlap, never gap.
      - Late-arriving data: don't widen the window with fudge factors ("reprocess
        last 3 days just in case") as the *only* mechanism — that's a cost/cover
        tradeoff that still misses data later than the fudge. Prefer watermark
        columns (below) or merge-based reprocessing of affected keys.
      
      ```python
      # BAD: wall-clock-relative — rerun tomorrow, get different data
      @task
      def extract():
          df = read_sql(f"SELECT * FROM orders WHERE created_at > "
                        f"'{datetime.now() - timedelta(days=1)}'")
      
      # GOOD: orchestrator-supplied logical interval — rerunnable forever
      @task
      def extract(data_interval_start, data_interval_end):
          df = read_sql(
              "SELECT ... FROM orders WHERE created_at >= %s AND created_at < %s",
              (data_interval_start, data_interval_end),
          )
      ```
      
      ## Incremental processing & watermarks
      
      - **Incremental by default for large sources.** Track a high-water mark on a
        monotonic column (`updated_at`, log sequence number, ingestion time) and
        select `> last_watermark AND <= new_watermark`. Persist the watermark
        transactionally **with** the load — watermark stored in a separate system
        updated after the write is a classic double/miss bug.
      
      ```sql
      -- BAD: watermark = "whatever was max at read time", stored elsewhere later.
      -- Crash between load and watermark-update => reprocess or skip.
      
      -- GOOD: bounded window, watermark committed with the data
      BEGIN;
      INSERT INTO staging_orders
        SELECT * FROM src.orders
        WHERE updated_at > (SELECT wm FROM etl.watermarks WHERE tbl='orders')
          AND updated_at <= :new_wm;          -- bounded above: deterministic
      UPDATE etl.watermarks SET wm = :new_wm WHERE tbl='orders';
      COMMIT;
      ```
      - `updated_at` maintained by application code is unreliable (bulk fixes skip
        it, clock skew). Prefer DB-generated change tracking or CDC (rules/03)
        for correctness-critical syncs; periodically reconcile row counts against
        the source either way.
      - **Late data policy is explicit per table:** how late is accepted (e.g.
        reprocess partitions up to 7 days back via merge), and what happens after
        (corrections batch, or documented "closed" partitions).
      - In dbt: incremental models must define `unique_key` (or use
        insert-overwrite strategy) and handle the full-refresh path; an
        incremental model that appends without a key is the same CRITICAL as
        above.
      
      ## Backfill design
      
      Backfills are a feature you design, not an emergency you improvise.
      
      - Every pipeline is runnable for an **arbitrary historical interval** with
        the same code path as the scheduled run (this falls out of logical-time
        parameterization).
      - **Bound parallelism.** Backfilling 3 years of daily partitions must not
        fire 1,000 concurrent tasks at the source DB or warehouse. Cap concurrency
        (e.g. Airflow `max_active_runs`/pools) and consider chunked
        ranges (monthly chunks of daily logic) for efficiency.
      - Order matters when models depend on prior intervals (cumulative tables,
        SCDs): declare `depends_on_past`-style constraints; otherwise allow
        parallel intervals.
      - Quality checks run on backfilled partitions too — a backfill that bypasses
        checks is how bad history gets written.
      - **AUDIT:** Ask "how would you reload March?" If the answer involves editing
        code or manual SQL, that's a HIGH finding.
      
      ## Orchestration discipline
      
      Applies to Airflow (3.x as of 2026), Dagster, Prefect, and kin.
      
      - **The DAG declares ALL dependencies.** Hidden coupling — task B reads a
        table task A writes, but no edge exists and B just runs "later by cron" —
        is a HIGH finding (cron-spaghetti). Prefer dataset/asset-aware scheduling
        (Airflow assets, Dagster assets): downstream runs *because* upstream
        produced, not because it's 6am. Partitioned assets (Airflow 3.2+) extend
        this per logical interval — downstream triggers per partition, unifying
        asset scheduling with the logical-time rule above.
      
      ```python
      # BAD: coupled by cron offsets and hope
      ingest_dag  = DAG("ingest",  schedule="0 5 * * *")
      mart_dag    = DAG("marts",   schedule="0 6 * * *")  # "ingest is done by 6, right?"
      
      # GOOD: data-aware — marts run when the asset is actually updated
      orders_stg = Asset("warehouse://staging/orders")
      ingest_dag = DAG("ingest", schedule="0 5 * * *")    # producer task outlets=[orders_stg]
      mart_dag   = DAG("marts",  schedule=[orders_stg])   # consumer triggered by production
      ```
      - **Tasks are stateless and idempotent**; orchestrator metadata is not a data
        store. No XCom-ing dataframes; pass references (paths, table names,
        intervals).
      - **Retries: limited and meaningful.** 2–3 retries with exponential backoff
        for transient failures. Retrying a deterministic transform error 10 times
        is noise; retrying forever masks outages. Alert on final failure, always.
      - **Timeouts on every task** sized from observed duration; a hung extraction
        blocking the daily run for 12 silent hours is an availability incident.
      - **SLAs/freshness alerts on outcomes, not just task failure:** "mart X not
        updated by 7am" pages someone even if no task technically failed (e.g.
        scheduler outage, upstream never triggered).
      - **No logic in the orchestrator that belongs in the transform.** Operators
        orchestrate; SQL/code transforms. Business logic hidden in DAG Python is
        untestable and invisible to lineage.
      - **Dependency-aware vs event-driven scheduling:** time-based schedules suit
        source-pull batch; asset/event-driven suits multi-team chains (no
        guessed cron offsets) and arrival-driven loads (file lands → run).
        Event-driven chains still need end-to-end freshness SLAs, since "nothing
        triggered" looks like success.
      
      ## dbt-style transformation discipline
      
      (Applies to dbt and equivalents — SQLMesh, etc. dbt note: the Fusion engine is
      in preview (preparing for GA) and dbt Core 2.0, built on the Fusion foundation,
      is in alpha as of mid-2026; don't hard-require Fusion-only features yet.)
      
      - **Tests on every model that matters:** at minimum `unique` + `not_null` on
        the primary key of every core/mart model, relationship tests on critical
        FKs, accepted-values on enums. Severity-tier them (rules/04).
      - **No `SELECT *` crossing model boundaries** (staging enumerates; marts
        list final columns). `*` within a CTE chain of one model is fine.
      - **Documentation is part of the model:** description + column docs on all
        mart models; exposures declared for dashboards/consumers so impact
        analysis works.
      - One model = one purpose; no 1,500-line mega-models. Use intermediate
        models; materialize hot paths as tables, cheap glue as views/ephemeral.
      - **CI runs build + tests on changed models and their downstream**
        (state/defer-based selection) against a non-prod target before merge.
        rules/06 covers deployment.
      - Pin package and adapter versions; reproducible builds.
      
      ## Audit checklist
      
      - [ ] Every scheduled write is partition-overwrite, MERGE, or full
            rebuild — zero blind appends outside raw landing?
      - [ ] Writes atomic per run (no separately-failable delete-then-insert)?
      - [ ] All tasks parameterized by logical interval; no `NOW()`-relative
            filters in scheduled code?
      - [ ] Watermarks persisted atomically with the data they describe?
      - [ ] Late-data policy documented per incremental table?
      - [ ] Backfill: arbitrary interval, same code path, bounded parallelism,
            checks still run?
      - [ ] DAG edges match actual data dependencies (no cron-offset coupling)?
      - [ ] Retries bounded with backoff; timeouts set; final-failure alerts
            routed to an owner?
      - [ ] Freshness/SLA alerting on outputs, independent of task success?
      - [ ] dbt (or equivalent): key tests on every core/mart model, no
            cross-model `SELECT *`, exposures declared, CI builds changed models
            before merge?
    • 03-streaming-and-cdc.md 10.3 KB
      # 03 — Streaming & CDC
      
      Streaming is the most expensive way to move data. It buys latency and pays
      in operational complexity, harder testing, and harder reprocessing. Make it
      earn its place.
      
      ## When streaming is justified
      
      - **Require a named consumer with a sub-minute (or low-minutes) latency
        need:** fraud/abuse decisions, operational dashboards driving immediate
        action, ML features that decay in minutes, user-facing freshness.
      - "The business wants real-time" usually means "the daily batch is too
        slow." Try 15-minute or hourly micro-batch first — same orchestration,
        same idempotent batch semantics, ~10x less operational surface.
      - **Streaming is also legitimate as transport regardless of latency:** CDC
        replication (below) and high-volume event ingestion where a durable log
        beats files. Transport-streaming + batch-transform is a sound and common
        shape.
      - **AUDIT:** A Flink/Kafka Streams job whose only consumer is an hourly
        dashboard = MEDIUM (cost/complexity without benefit). No documented
        latency requirement for a streaming component = ask; absence of any
        consumer needing it = finding.
      
      ## Kafka-style log fundamentals
      
      (Kafka 4.x is current — ZooKeeper is gone, KRaft-only; the KIP-848 consumer
      rebalance protocol is GA in 4.0+ and consumers opt in via
      `group.protocol=consumer`. Same fundamentals apply to Pulsar/Kinesis/Redpanda.)
      
      - **Ordering exists only within a partition.** Choose the partition key as
        the entity whose event order matters (`order_id`, `user_id`,
        `aggregate_id`). Random/round-robin keys on a topic where consumers assume
        per-entity order is a CRITICAL correctness finding.
      - **Key skew:** one hot key = one hot partition = one maxed consumer no
        matter how many instances you add. Check partition-level lag/throughput
        for skew before scaling out.
      - **Partition count is concurrency ceiling** for a classic consumer group
        (one consumer per partition max). Size with headroom (you can add
        partitions but that **reshuffles key→partition mapping**, breaking
        per-key ordering across the boundary — plan it, don't improvise it).
        KIP-932 "share groups" (queue semantics, more consumers than partitions,
        per-record acks) are production-ready since Kafka 4.2 but sacrifice
        per-key ordering — only for true queue workloads.
      - **Consumer groups & offsets:** commit offsets only after the message's
        effects are durably stored. Auto-commit-on-poll means a crash between
        commit and processing **loses data**; commit-after-process means
        duplicates on crash — which is why sinks must be idempotent (below).
      - **Rebalancing pitfalls:** long processing between polls (`max.poll.interval.ms`
        exceeded) gets consumers evicted, causing rebalance storms; slow
        startup + eager rebalance causes stop-the-world pauses. Use cooperative/
        KIP-848 protocols, keep per-poll work bounded, and process slow items
        async with pause/resume rather than blocking poll.
      - **Retention is a contract:** consumers must be able to be down for less
        than retention and recover. Lag monitoring with alerts on every production
        group is mandatory; lag approaching retention = imminent data loss.
      
      ## Exactly-once reality
      
      "Exactly-once" end-to-end is achieved as **effectively-once: at-least-once
      delivery + idempotent application of effects.** Audit the sink, not the
      producer flag.
      
      - **Idempotent producer** (`enable.idempotence=true`, default in modern
        clients): prevents broker-side duplicates from producer retries. Necessary,
        nowhere near sufficient.
      - **Kafka transactions / EOS:** give atomic consume-transform-produce
        *within Kafka* (Kafka→Kafka pipelines, Streams `processing.guarantee=
        exactly_once_v2`). The moment data leaves Kafka for a warehouse, lake, or
        API, transactions stop covering it.
      - **At the sink, make redelivery harmless:**
        - MERGE/upsert on a natural or deterministic key (event ID,
          `topic-partition-offset`).
        - Or atomic write+offset: store consumed offsets in the same transaction
          as the data (e.g. in the target DB), resume from stored offsets.
        - Or rely on the connector's documented mechanism (e.g. table-format sinks
          committing offsets inside the table commit) — verify, don't assume.
      
      ```python
      # BAD: duplicates on every crash between write and commit
      for msg in consumer:
          warehouse.insert("events", msg.value)      # append
          consumer.commit()                          # separate failure domain
      
      # GOOD: redelivery-proof — offset stored atomically with the data
      for batch in consumer.batches():
          with target_db.transaction() as tx:
              tx.merge("events", batch.rows, key="event_id")     # idempotent
              tx.upsert("kafka_offsets", batch.tp, batch.last_offset)
      # on startup: seek(stored_offset + 1); Kafka's committed offset is advisory
      ```
      - **AUDIT:** Consumer writes appends to a warehouse table and commits Kafka
        offsets separately = duplicates on every crash/rebalance = CRITICAL if the
        table feeds metrics. "We have exactly-once because idempotent producer is
        on" = misunderstanding; check the sink.
      
      ## Schema registry & evolution
      
      - **Every production topic has a registered schema** (Avro/Protobuf/JSON
        Schema). Schemaless JSON topics shared across teams = HIGH; every consumer
        is one producer refactor away from breaking.
      - **Pick and enforce a compatibility mode.** `BACKWARD` (new schema reads
        old data: add optional fields, delete fields) lets consumers upgrade
        after producers — the common default. `FORWARD` = consumers first.
        `FULL` for long-lived shared topics. Never `NONE` in prod.
      - Breaking changes (rename, retype, semantic change) = **new topic** (or
        contract-versioned subject) + migration window, not an in-place break.
      - Defaults on new fields; never reuse field IDs/positions (Protobuf/Avro).
      
      ## CDC patterns
      
      - **Log-based CDC (Debezium-class, 3.x current) is the default** for
        replicating OLTP into the analytical platform: reads the WAL/binlog,
        emits ordered change events, near-zero source impact. Query-based
        ("`SELECT WHERE updated_at >`") misses deletes and intermediate states —
        acceptable only for append-only sources.
      - **Snapshot + stream:** initial consistent snapshot, then stream from the
        log position captured at snapshot start. Re-snapshot procedure must exist
        (incremental snapshots) for adding tables or recovering from gaps.
      - **Deletes & tombstones:** CDC delete events must be **applied** downstream
        (merge-delete or soft-delete flag), and Kafka tombstones (null payloads on
        compacted topics) must be handled by every consumer. A lake table fed by
        CDC where deletes are dropped silently diverges from source = CRITICAL.
      - **Apply layer:** CDC streams are change *logs*; downstream either stores
        the log (append, then dedupe/window to current state in SQL) or maintains
        a mirror via MERGE keyed on PK ordered by LSN/commit timestamp. Out-of-order
        application (e.g. merging on wall-clock `updated_at` with ties) silently
        resurrects deleted/stale rows.
      
      ```sql
      -- GOOD: latest-state mirror from a CDC change log, deletes applied,
      -- ordered by log position (LSN), not wall-clock
      MERGE INTO mirror.customers t
      USING (
        SELECT * FROM cdc.customers_changes
        QUALIFY row_number() OVER (PARTITION BY id ORDER BY source_lsn DESC) = 1
      ) s ON t.id = s.id
      WHEN MATCHED AND s.op = 'd' THEN DELETE
      WHEN MATCHED THEN UPDATE SET ...
      WHEN NOT MATCHED AND s.op != 'd' THEN INSERT ...;
      ```
      - **Outbox pattern** for application-emitted events (avoiding dual-write
        inconsistency) is owned by `sota-architecture` — use it; don't tail
        business tables to fake events.
      - Schema changes on source tables flow through CDC: test ADD COLUMN and
        type-widening paths; alert on incompatible DDL rather than silently
        dropping fields.
      
      ## Watermarks, windowing, late data
      
      - **Event time, not processing time,** for any business aggregation.
        Processing-time windows shift numbers whenever the pipeline lags.
      - A **watermark** declares "events older than T are no longer expected" and
        triggers window emission. Set allowed lateness from measured event-delay
        distributions (p99/p999), not guesses.
      - Decide explicitly what happens to later-than-watermark events: drop +
        count (alert on the count), side-output to a corrections path, or
        re-emit updated window results (consumers must then handle retractions/
        upserts). Silent drop with no metric = HIGH.
      - Window types: tumbling for periodic aggregates, sliding for rolling
        metrics, session for activity grouping. State for long/huge windows needs
        TTLs — unbounded keyed state is a slow OOM.
      
      ## DLQ & poison messages
      
      - **Every streaming consumer has a poison-message policy.** Default: retry
        N times (with backoff), then route the message + error metadata +
        original topic/partition/offset to a DLQ topic, and continue. A consumer
        that crash-loops on one bad message takes the whole partition hostage =
        HIGH.
      - DLQs are monitored (alert on arrival rate) and have a replay path back to
        the source topic after fix. An unmonitored DLQ is a data black hole —
        MEDIUM.
      - Never DLQ-and-forget messages whose absence corrupts aggregates; for
        those, halt and page instead (block-tier, by analogy with rules/04).
      
      ## Backpressure
      
      Consumers must degrade by slowing intake (pause/resume, bounded buffers),
      not by buffering unboundedly in memory. Mechanics and patterns are owned by
      `sota-async-concurrency`; in Kafka terms: bound in-flight work, watch lag as
      the system-level backpressure signal, scale consumers before lag approaches
      retention.
      
      ## Audit checklist
      
      - [ ] Each streaming component has a named consumer/latency requirement, or
            is justified as transport?
      - [ ] Partition keys match the entity whose ordering matters; skew checked?
      - [ ] Offsets committed only after effects are durable; auto-commit
            semantics understood?
      - [ ] Sinks idempotent (merge key / transactional offsets) — exactly-once
            claims verified at the sink?
      - [ ] Consumer lag monitored with alerts on all prod groups; retention >
            max tolerable downtime?
      - [ ] Schemas registered with enforced compatibility mode; breaking changes
            via new topic/version?
      - [ ] CDC: log-based for sources with deletes; deletes/tombstones applied
            downstream; ordering by LSN not wall-clock; re-snapshot path exists?
      - [ ] Aggregations on event time with explicit watermark/lateness policy;
            late-drop counted and alerted?
      - [ ] DLQ per consumer, monitored, with replay procedure; no crash-looping
            on poison messages?
      - [ ] Keyed state has TTLs; consumer memory bounded under lag?
    • 04-data-quality-and-contracts.md 9.7 KB
      # 04 — Data Quality & Contracts
      
      Bad data that flows is worse than no data: dashboards stay green, decisions
      get made, and trust — the only real product of a data platform — erodes.
      Quality is enforced at boundaries and tiered by consequence.
      
      ## Data contracts at the producer boundary
      
      A contract is **schema + semantics + SLOs**, owned by the producer, enforced
      mechanically. Schema alone is half a contract.
      
      - **Schema:** field names, types, nullability, enums — registered and
        compatibility-checked (registry for streams, declared source schemas or
        dbt model contracts for batch).
      - **Semantics:** grain ("one row per order line"), key uniqueness, units and
        timezone (`amount` in cents? `created_at` in UTC?), enum meanings,
        delete behavior (hard/soft/tombstone).
      - **SLOs:** freshness ("available by 06:00 UTC", "p99 event lag < 5 min"),
        completeness, and a deprecation policy (notice period for breaking
        changes).
      - **Enforcement beats documentation:** CI on the producer side fails builds
        that break the contract (schema registry compat check, dbt `contract:
        enforced`, contract-test suites). A contract in a wiki nobody's CI reads =
        LOW-value; consumers breaking on producer deploys despite a "contract" =
        HIGH.
      - Contracts go where ownership changes hands: source→platform and
        mart→consumer. Don't contract every internal model — that's change
        friction without protection.
      
      ```yaml
      # GOOD: dbt model contract on a mart boundary
      models:
        - name: fct_orders
          config:
            contract: {enforced: true}
          columns:
            - name: order_line_id
              data_type: bigint
              constraints: [{type: not_null}]
              tests: [unique]
            - name: amount_usd_cents   # unit in the name beats unit in the wiki
              data_type: bigint
      ```
      
      ## Expectation testing — the core battery
      
      Every core/mart model carries checks from this battery; critical ones carry
      all that apply.
      
      - **Freshness:** `max(loaded_at)` within SLO. The single highest-value
        check — most data incidents are "it silently stopped."
      - **Volume:** row count of the latest interval within expected band
        (absolute bounds or relative to trailing window, e.g. ±50% of 7-day
        median; use week-over-week comparisons for weekly-seasonal data).
        Zero-row intervals almost always block.
      - **Nulls:** `not_null` on keys and critical measures. Watch *null-rate
        jumps* on optional columns too — a field going 2%→40% null is an upstream
        break that `not_null` won't catch.
      - **Uniqueness:** primary/grain key unique. Duplicate grain = every
        downstream aggregate inflated = block-tier, always.
      - **Referential integrity:** facts' FKs resolve to dimensions (or to an
        explicit `unknown` member). Orphaned facts silently vanish from
        inner-join dashboards.
      - **Accepted values / ranges:** enums in the known set; amounts within sane
        bounds; dates not in the future (a `2106-02-07` timestamp is an epoch bug,
        not a fact).
      - **Distribution drift:** track means/quantiles/category mix on
        business-critical columns; alert on significant shift. Warn-tier — drift
        is a question, not a verdict.
      
      ```yaml
      # GOOD: minimum battery on a critical mart, tiered (dbt syntax; Soda/GX
      # equivalents exist for all of these)
      models:
        - name: fct_orders
          tests:
            - dbt_utils.recency:            # freshness — blocks
                datepart: hour, field: loaded_at, interval: 26
            - dbt_utils.expression_is_true: # volume — warns, human triages
                expression: >
                  (SELECT count(*) FROM {{ this }} WHERE order_date = current_date - 1)
                  BETWEEN 0.5 * {{ var("orders_daily_median") }}
                      AND 2.0 * {{ var("orders_daily_median") }}
                config: {severity: warn}
          columns:
            - name: order_line_id
              tests: [unique, not_null]     # grain — blocks, always
            - name: customer_id
              tests:
                - relationships: {to: ref('dim_customer'), field: customer_id}
      ```
      
      ## Severity tiers: block vs warn
      
      Two failure modes to avoid: everything blocks (one flaky check halts the
      platform nightly, team starts ignoring red) and nothing blocks (corrupt data
      flows downstream while a Slack channel nobody reads fills up).
      
      - **Block (fail task, stop downstream):** contract violations, duplicate
        keys, null keys, zero-row loads, broken referential integrity on critical
        marts. Criterion: *would you rather show stale data than this data?* If
        yes → block. Stale + alert beats wrong + silent.
      - **Warn (alert, let flow):** drift, moderate volume anomalies, null-rate
        shifts on non-key columns, slow-burn issues.
      - Every warn goes to an **owned** channel with a triage expectation. A
        warn-tier check failing for 3+ weeks untriaged = the check is dead;
        fix it or delete it (MEDIUM finding).
      - Implement tiers natively: dbt test `severity: error|warn`, Great
        Expectations / Soda / pandera equivalents all support it.
      
      ## Anomaly detection caution
      
      ML-based anomaly detection on data metrics (auto-thresholds on volume,
      freshness, distributions) is a **supplement, not a substitute** for explicit
      checks.
      
      - It cannot block (too many false positives) and it can't encode semantics
        ("orders must never be negative").
      - Untuned anomaly monitors produce alert fatigue that kills the *real*
        alerts. Budget alerts-per-week per owner; tune or cut anything over.
      - Right order: explicit battery on what you know must hold → anomaly
        detection for the unknown-unknowns on a *small* set of business-critical
        tables.
      
      ## Lineage
      
      - Table-level lineage is table stakes — it falls out of dbt/orchestrator
        graphs. Use it for impact analysis ("what breaks if I change this?") and
        incident blast-radius.
      - **Column-level lineage pays where:** PII propagation must be traced
        (which marts contain `email`? — see `sota-privacy-compliance`),
        regulated-metric provenance, and deprecating wide legacy tables. Don't
        buy/maintain column-level everywhere for its own sake.
      - Lineage that requires manual upkeep will rot; derive it from code
        (parsed SQL, orchestrator metadata) or don't claim it.
      
      ## Data incident response
      
      When bad data ships, the order is: **stop the spread → communicate →
      quarantine → fix → reprocess → verify → postmortem.**
      
      1. **Stop the spread:** pause downstream pipelines/reverse-ETL consuming the
         bad data. Blocking checks should have done this; do it manually if not.
      2. **Communicate first, fix second:** notify downstream owners and stamp
         affected dashboards ("data since DATE under investigation") *before*
         debugging. Consumers acting on known-bad data is the real damage.
      3. **Quarantine:** snapshot/copy bad partitions for forensics; with
         Iceberg/Delta, note the pre-incident snapshot ID — time travel is your
         forensic record (until retention expires it; act fast, see rules/05).
      4. **Fix and reprocess** through the normal backfill path (rules/02) —
         never hand-`UPDATE` a mart; if raw is intact, re-derive. This is where
         idempotency pays its rent.
      5. **Verify** with the same checks that should have caught it — and add the
         check that was missing.
      6. **Postmortem** for trust-damaging incidents: which boundary lacked which
         check; fix the class, not the instance.
      - **AUDIT:** No documented incident path / no way to mark data as
        known-bad to consumers = MEDIUM; becomes HIGH for platforms feeding
        financial or operational decisions.
      
      ## Testing pipelines (CI)
      
      Quality checks validate *data in production*; tests validate *logic before
      merge*. You need both — a check can't catch a bug that produces plausible
      numbers.
      
      - **Unit-test transforms with fixture data:** tiny handcrafted inputs +
        expected outputs, covering edge cases (nulls, duplicates, late rows,
        timezone boundaries, empty input). dbt unit tests (1.8+), pytest +
        DuckDB/Polars for Python transforms, or framework-native harnesses for
        streaming (e.g. topology test drivers).
      
      ```yaml
      # GOOD: dbt unit test — logic verified pre-merge, no warehouse data needed
      unit_tests:
        - name: dedupe_keeps_latest_version
          model: stg_orders
          given:
            - input: source('shop', 'orders')
              rows:
                - {order_id: 1, status: "open",   _loaded_at: "2026-01-01"}
                - {order_id: 1, status: "closed", _loaded_at: "2026-01-02"}
          expect:
            rows:
              - {order_id: 1, order_status: "closed"}
      ```
      - **DuckDB as the CI workhorse:** runs most ANSI-ish SQL in-process — fast
        warehouse-free tests for portable SQL. Dialect-specific SQL still needs a
        dev schema in the real engine (cheap, ephemeral, per-PR).
      - **CI on sample data:** build changed models + downstream against
        representative samples; full-volume runs are for staging, not per-PR.
      - Pure functions are testable functions: keep transforms free of hidden
        `NOW()`/env reads (rules/02's logical-time rule makes this automatic).
      - **AUDIT:** A transform repo with zero tests where a one-character SQL
        change can silently flip a company metric = HIGH.
      
      ## Audit checklist
      
      - [ ] Producer-boundary contracts exist (schema + semantics + SLOs) and are
            CI-enforced, not wiki-only?
      - [ ] Every core/mart model: freshness, volume, unique-key, not-null-key
            checks at minimum?
      - [ ] Checks explicitly tiered; block-tier actually stops downstream
            (not just alerts)?
      - [ ] Zero-row and duplicate-key conditions block on critical marts?
      - [ ] Warn alerts owned and triaged — no weeks-old ignored failures?
      - [ ] Referential integrity checked between facts and dimensions?
      - [ ] Anomaly detection (if any) supplements explicit checks and isn't an
            alert-fatigue source?
      - [ ] Lineage derivable from code; column-level where PII/regulatory needs
            it?
      - [ ] Incident path documented: pause downstream, notify, quarantine,
            reprocess via backfill, verify?
      - [ ] Transform logic unit-tested with fixtures; CI builds changed models
            pre-merge?
    • 05-storage-and-performance.md 9.1 KB
      # 05 — Storage & Performance
      
      Analytical performance is mostly **scan avoidance**: read fewer files, fewer
      row groups, fewer columns, fewer bytes. Every rule here is a variation on
      that theme, and every one is also a cost lever.
      
      ## Parquet internals that matter
      
      - **Row groups are the pruning unit.** Each carries min/max stats per
        column; engines skip row groups whose stats exclude the predicate
        (predicate pushdown). This only works if data is **sorted/clustered** on
        the filtered column — random layout makes every min/max range span
        everything, and pruning dies. Target row groups ~128 MB-ish (engine
        defaults are sane; pathological cases are tiny row groups from trickle
        writes).
      - **Columnar projection:** only referenced columns are read. `SELECT *` in
        pipelines defeats the format's core advantage (and rules/01 bans it across
        layers anyway).
      - **Dictionary encoding + page stats** make low-cardinality string filters
        cheap; sorted data compresses dramatically better (run-length/delta).
        Sorting is simultaneously a performance and a storage-cost move.
      - **File sizing: target ~128 MB–1 GB per file.** Two failure modes:
        - *Small files* (the classic killer): thousands of KB-sized files from
          streaming/micro-batch/per-key writes turn metadata listing and open
          overhead into the dominant cost. Symptom: query time flat as data
          shrinks. Fix: compaction (below) and buffered/batched writes.
        - Giant single files limit read parallelism.
      - **AUDIT:** `ls` a few partitions. Median file size under ~10 MB on a
        frequently-queried table = MEDIUM, HIGH at scale. CSV/JSON as the *query*
        layer (not just landing) = MEDIUM — convert to Parquet at staging.
      
      ## Partitioning vs clustering/sorting
      
      - **Partitioning** (directory/metadata-level split, one value-set per
        partition): for the **coarse, always-filtered** dimension — almost always
        event date. Rules:
        - Partition count sanity: aim for partitions ≥ ~1 GB. Don't partition
          small tables at all.
        - **Never partition on high-cardinality columns** (`user_id`,
          `order_id`) — that's the small-files problem by construction =
          HIGH finding.
        - Iceberg uses *hidden* partitioning via transforms (`days(ts)`,
          `bucket(N, id)`) — queries on the raw column prune automatically, and
          partition schemes can evolve without rewriting old data.
      - **Clustering/sorting within partitions:** for the second-tier filter
        columns (`customer_id`, `country`). Sort on write (`ORDER BY` in CTAS,
        Iceberg sort orders) or use the platform's clustering (Delta liquid
        clustering, warehouse cluster keys) so min/max pruning works inside
        partitions.
      - Rule of thumb: **partition on one time column; sort/cluster on 1–3 query
        columns; stop.** Multi-level partitioning (`date/country/category`) is
        usually a small-files generator.
      
      ```sql
      -- BAD: high-cardinality partitioning → millions of tiny files
      CREATE TABLE events PARTITIONED BY (user_id) ...;
      
      -- GOOD (Iceberg): coarse hidden time partition + bucket + sort
      CREATE TABLE events (event_ts timestamp, user_id bigint, ...)
      PARTITIONED BY (days(event_ts), bucket(32, user_id));
      ALTER TABLE events WRITE ORDERED BY (user_id);
      ```
      
      ## Table formats: Iceberg / Delta
      
      Open table formats add ACID commits, snapshots, and schema evolution on
      object storage. Status (verified mid-2026): **Iceberg format v3** is ratified
      and GA across major engines (deletion vectors, row lineage; engine support
      for v3 features still varies — confirm your engines before enabling v3
      features). **Delta Lake 4.x** is current (variant type, collations; deletion
      vectors and liquid clustering mature from 3.x).
      
      - **Snapshots & time travel:** every commit is a snapshot; you can query
        `AS OF` for debugging, incident forensics (rules/04), and WAP (rules/06).
        Time travel is bounded by snapshot retention — it is **not a backup
        strategy**.
      - **Schema evolution:** safe in-place: add columns, widen types, rename
        (Iceberg tracks by field ID; renames are metadata-only). Still forbidden
        by *your* contracts without coordination (rules/04): downstream code keys
        on names.
      - **Row-level changes:** merge-on-read (delete files/deletion vectors) makes
        MERGE/DELETE cheap at write time but accumulates read-time debt;
        copy-on-write is the reverse. CDC-heavy tables on merge-on-read **require**
        regular compaction of delete files or reads degrade steadily.
      - **Maintenance is mandatory, scheduled, from day one:**
        - *Data compaction* (Iceberg `rewrite_data_files`, Delta `OPTIMIZE`):
          fixes small files and applies sort orders.
        - *Snapshot expiry* (`expire_snapshots` / `VACUUM`): unexpired snapshots
          = unbounded storage growth; retention window = your time-travel and
          concurrent-reader safety window (don't vacuum to zero).
        - *Manifest/metadata cleanup + orphan file removal* on a slower cadence.
        - **AUDIT:** An Iceberg/Delta table with no scheduled maintenance job =
          MEDIUM, HIGH for streaming/CDC-fed tables (they degrade fastest).
      
      ```sql
      -- GOOD: scheduled weekly maintenance (Iceberg/Spark procedures)
      CALL catalog.system.rewrite_data_files(
        table => 'db.events', strategy => 'sort',
        options => map('target-file-size-bytes', '536870912'));
      CALL catalog.system.expire_snapshots(
        table => 'db.events', older_than => now() - INTERVAL 7 DAYS,
        retain_last => 20);
      CALL catalog.system.remove_orphan_files(table => 'db.events');
      
      -- Delta equivalents: OPTIMIZE events; VACUUM events RETAIN 168 HOURS;
      ```
      - Concurrent writers: optimistic concurrency means conflicting commits
        retry; partition-overlapping concurrent writes (e.g. backfill + scheduled
        run on the same partitions) need coordination, not hope.
      
      ## Compression
      
      - **zstd is the modern default** for Parquet (better ratio than snappy at
        similar read speed; level ~3 is the sweet spot — high levels buy little
        for analytics and cost write CPU). snappy remains fine; gzip is
        legacy-compat only; uncompressed is never right.
      - Biggest compression lever is **sorting**, not codec choice (see above).
      - Don't double-compress (gzip-ing Parquet files) and don't ship
        `.csv.gz` as a query layer — gzip CSV isn't splittable.
      
      ## Cost levers (warehouse credit burn & lake scan cost)
      
      Cost in modern platforms ≈ bytes scanned × frequency + compute time ×
      concurrency. Attack in this order:
      
      1. **Prune more:** verify top queries actually hit partition/cluster
         pruning (`EXPLAIN` / query profile: partitions scanned vs total). A
         dashboard filter that wraps the partition column in a function can
         disable pruning — keep predicates sargable (see `sota-databases`).
      
         ```sql
         -- BAD: function over the partition column — full scan on many engines
         WHERE date(event_ts) = '2026-06-01'
         -- GOOD: range predicate on the raw column — prunes
         WHERE event_ts >= '2026-06-01' AND event_ts < '2026-06-02'
         ```
      2. **Scan less per query:** incremental models instead of full rebuilds
         (rules/02); pre-aggregate hot dashboard queries into small marts;
         column pruning.
      3. **Run less often:** match schedule to consumer need. An hourly rebuild
         feeding a daily-reviewed dashboard burns 24x the spend for zero value =
         classic MEDIUM finding.
      4. **Materialization tradeoffs:** table for hot/expensive paths, view for
         cheap glue, incremental for large facts. A view chain 6 deep recomputed
         by every dashboard query is a hidden multiplier; conversely,
         materializing everything pays storage + build time for unread tables.
      5. **Right-size and auto-suspend compute:** warehouses sized for the p99
         job running 24/7 for p50 work; aggressive auto-suspend; separate
         ETL/BI/ad-hoc compute pools so one team's scan storm doesn't queue
         everyone (and so cost is attributable per team).
      6. **Watch the spend:** per-pipeline/per-team cost attribution and alerts
         on week-over-week jumps. Unattributed warehouse spend grows until
         someone panics.
      - **AUDIT (quick wins, in order):** full-refresh models on large sources;
        queries scanning all partitions; small-files tables; oversized always-on
        compute; orphaned pipelines feeding dashboards nobody opens (check BI
        view counts — deleting a pipeline is the best optimization).
      
      ## Audit checklist
      
      - [ ] Query layer is columnar (Parquet/native), not CSV/JSON?
      - [ ] File sizes healthy (~128 MB–1 GB median); no small-files
            accumulation on hot tables?
      - [ ] Partitioned on a coarse time column only; no high-cardinality or
            deep multi-level partitioning?
      - [ ] Sort/cluster order defined matching top query predicates; pruning
            verified via query profiles?
      - [ ] Iceberg/Delta: compaction + snapshot expiry + orphan cleanup
            scheduled on every table; CDC-fed merge-on-read tables compacted
            aggressively?
      - [ ] Snapshot retention consciously chosen (forensics window vs storage)?
      - [ ] Concurrent-writer conflicts (backfill vs scheduled) coordinated?
      - [ ] zstd (or justified alternative) everywhere; no double compression?
      - [ ] Hot dashboards served from pre-aggregated marts, not view chains over
            raw facts?
      - [ ] Schedules match consumer freshness needs; no hourly builds of
            daily-read data?
      - [ ] Compute auto-suspends, pools separated by workload, cost attributed
            per pipeline/team with trend alerts?
    • 06-operations-and-governance.md 9 KB
      # 06 — Operations & Governance
      
      A pipeline that works is a prototype; a pipeline that is deployable,
      observable, recoverable, and access-controlled is a product. This file
      covers the gap.
      
      ## Environments for data
      
      Code environments are easy; **data** environments are where platforms cheat.
      
      - **Three-way isolation as the baseline:** dev (engineers iterate), staging/
        CI (automated validation), prod (consumers). Separate
        databases/schemas/buckets *and* separate credentials — dev code holding
        prod write credentials is a CRITICAL finding waiting for a `--target`
        typo.
      - **Dev reads (a copy or view of) prod-shaped data, never writes prod.**
        Patterns, best first:
        - *Zero-copy clones / shallow copies* (warehouse clones, table-format
          snapshots): instant, cheap, real data shape.
        - *Sampled subsets* refreshed on schedule: fast dev iteration; document
          the sampling so devs don't "discover" sampling artifacts.
        - *Masked copies* where PII rules require (PII handling itself:
          `sota-privacy-compliance`). Production PII in dev environments = HIGH.
      - Synthetic-only dev data is a trap: pipelines pass on clean synthetic rows
        and die on real-world nulls/dupes/encodings. Use sampled-real (masked)
        where at all possible.
      - **Schema parity:** dev/staging schemas built by the same code path
        (migrations / dbt) as prod — never hand-maintained copies that drift.
      - Per-PR ephemeral schemas (e.g. `pr_1234` in a CI database) beat one
        shared, perpetually-dirty "staging" everyone fights over.
      
      ## Deploying pipeline changes
      
      Pipeline code is software: version control, review, CI (rules/02, 04). The
      extra dimension is that deploys change *tables others read*.
      
      ### Write-Audit-Publish (WAP) — default for risky changes
      
      Never let unvalidated data become visible to consumers.
      
      1. **Write** the new run's output to an unpublished location: a staging
         table, an Iceberg branch (engine support varies — verify yours), or an
         unswapped table version.
      2. **Audit:** run the block-tier quality battery (rules/04) against the
         unpublished output — plus diff-vs-current checks for logic changes (row
         counts, key metric deltas within tolerance).
      3. **Publish** atomically only on pass: branch fast-forward, partition
         swap, view-pointer flip, or atomic table swap.
      
      Failed audit = nothing published, consumers see last good data + a freshness
      note. This converts "we shipped wrong numbers" into "we shipped late."
      
      ```sql
      -- BAD: write directly to the consumer-facing table, validate afterwards
      INSERT OVERWRITE fct_orders PARTITION (ds='2026-06-11') SELECT ...;
      -- (checks run later; consumers already saw whatever landed)
      
      -- GOOD: WAP — write unpublished, audit, publish atomically
      INSERT OVERWRITE fct_orders__wap PARTITION (ds='2026-06-11') SELECT ...;
      -- run block-tier checks against fct_orders__wap; only then:
      ALTER TABLE fct_orders REPLACE PARTITION (ds='2026-06-11')
        FROM fct_orders__wap;   -- engine-specific: Iceberg branch fast-forward,
                                -- partition exchange, or atomic view re-point
      ```
      
      ### Blue-green tables for breaking rebuilds
      
      For logic changes that rewrite a whole mart: build `fct_orders__v2`
      alongside, validate (full quality battery + reconciliation against v1 with
      expected-diff documentation), then repoint the consumer-facing **view** to
      v2. Keep v1 queryable through the agreed deprecation window for rollback and
      consumer comparison.
      
      - Corollary: **consumers read views, not physical tables.** The
        view layer is your indirection for blue-green, refactors, and renames.
      - Rollback plan is part of the deploy: previous table/snapshot retained
        until the change has survived N cycles.
      - **AUDIT:** Logic changes that rewrite consumer-facing tables in place
        with no validation gate = HIGH; with no rollback artifact = HIGH.
      
      ## Access control
      
      (Mechanics of RBAC/row policies live with the engine — `sota-databases`;
      PII classification and lawful basis — `sota-privacy-compliance`. This is the
      data-platform layer.)
      
      - **Grant to roles per layer, least privilege:** consumers get read on
        marts only (raw/staging contain unmodeled PII and un-cleaned data);
        pipelines get write only to their layer; humans don't share the service
        account. BI service accounts with `SELECT ANY` on raw = HIGH.
      - **Column-level controls** (masking policies, secure views) for PII columns
        that must coexist with analytics — masked by default, unmasked for an
        audited role. **Row-level** policies for multi-tenant or
        regional-restriction marts.
      - Tag/classify PII columns in the catalog at ingestion and let policies key
        off tags — per-column manual grants don't survive schema growth.
      - Audit logging on access to sensitive marts; review grants on a schedule
        (orphaned humans, over-privileged services).
      
      ## Retention & GDPR deletion in immutable-ish stores
      
      "Immutable raw forever" collides with deletion duties. Design for erasure
      up front; retrofitting is brutal.
      
      - **Know where every subject's data lives:** PII column tags + lineage
        (rules/04) make "find all copies" answerable. Copies include: raw,
        staging, marts, snapshots/time-travel, DLQs, dev clones, BI extracts.
      - **Lakehouse deletion that actually deletes:** `DELETE`/`MERGE` on
        Iceberg/Delta marks rows deleted in the *current* snapshot — the bytes
        live on in old snapshots/files until **snapshot expiry + compaction/
        vacuum** run. The erasure SLA must account for the full chain: delete →
        expire snapshots → rewrite/vacuum files. Deletion-vector/merge-on-read
        tables additionally need compaction to physically drop the rows.
      - **Kafka:** compacted topics honor tombstones (eventually — compaction
        lag); time-retention topics age data out. Long/infinite-retention topics
        containing PII need a deletion story or pseudonymization at produce time.
      - **Crypto-shredding** (per-subject encryption keys; erase the key to erase
        the data) is the pragmatic answer for deep-archive/backup layers where
        rewriting is infeasible — key management discipline required
        (`sota-secrets-management`).
      - Retention policies are **enforced by scheduled jobs, not by policy
        documents**: raw N months, snapshots N days, DLQs N days, dev clones
        auto-expire. Unbounded-retention PII stores = HIGH.
      - Deletion requests are pipelines too: idempotent, monitored, with
        completion SLO and verification query.
      
      ## Observability for pipelines
      
      (Generic telemetry stack: `sota-observability`. Data-specific layer here.)
      
      - **The four golden signals of data:** freshness (did it land on time?),
        volume (the right amount?), quality (checks green?), lineage-aware status
        (what's blocked downstream?). One dashboard, all critical marts,
        red/green per signal.
      - **Alert on consumer-facing symptoms, route to the owning team:** "mart X
        stale > SLO" beats 14 task-level alerts that all mean the same outage.
        Task-level detail belongs in the runbook drill-down, not the page.
      - Every alert has an **owner and a runbook link**. Unrouted alerts and
        alert channels with >daily noise = the platform is unmonitored in
        practice (MEDIUM, HIGH for critical marts).
      - Track **cost** as a first-class signal (rules/05): per-pipeline spend
        trend on the same dashboard — cost regressions are incidents too.
      - SLOs documented per critical mart (freshness target + measurement) and
        reviewed; an SLO nobody measures is decoration.
      
      ## Runbook discipline
      
      - Every production pipeline has a runbook answering: what it produces and
        for whom (links to contracts/SLOs); how to rerun/backfill an interval
        (exact command); known failure modes and fixes; upstream/downstream
        contacts; escalation path.
      - Runbooks live next to the code (repo `runbooks/` or per-DAG docs), are
        linked from every alert, and are updated as part of incident postmortems
        (rules/04). A runbook last touched two years before the last incident is
        fiction.
      - Test the runbook: a new on-call engineer should be able to execute a
        rerun from it without tribal knowledge. If only one person can operate a
        pipeline, that's a HIGH operational finding regardless of code quality.
      
      ## Audit checklist
      
      - [ ] Dev/staging/prod separated in storage *and* credentials; dev cannot
            write prod?
      - [ ] Dev data is cloned/sampled (masked where PII), refreshed by
            automation; schemas built from the same code as prod?
      - [ ] Risky changes gated by WAP or blue-green + reconciliation; consumers
            read through views?
      - [ ] Rollback artifact (previous table/snapshot) retained for every
            consumer-facing change?
      - [ ] Layer-based grants: no consumer/BI access to raw; no shared human
            use of service accounts?
      - [ ] PII columns tagged; masking/row policies keyed off tags; access to
            sensitive marts audit-logged?
      - [ ] GDPR erasure path covers snapshots, compaction, DLQs, dev clones,
            and Kafka retention — with a measured SLA?
      - [ ] Retention enforced by scheduled jobs for every layer (raw, snapshots,
            DLQ, dev clones)?
      - [ ] Freshness/volume/quality dashboard for critical marts; alerts
            symptom-based, owned, runbook-linked, low-noise?
      - [ ] Per-pipeline cost visible with trend alerts?
      - [ ] Runbooks current, co-located with code, executable by a newcomer?
  • SKILL.md 7.9 KB
    ---
    name: sota-data-engineering
    description: >-
      State-of-the-art data engineering rules (2026) for building and auditing
      data pipelines and analytics infrastructure. Covers architecture and
      modeling (ELT, lakehouse vs warehouse, dimensional models, medallion
      layering), pipeline and orchestration discipline (idempotency, incremental
      loads, backfills, dbt-style transformations), streaming and CDC (Kafka,
      exactly-once reality, schema evolution, Debezium-style capture), data
      quality and contracts, columnar storage and table-format performance
      (Parquet, Iceberg, Delta), and pipeline operations/governance. Use when
      designing, implementing, reviewing, or auditing batch/streaming pipelines,
      warehouses, or lakehouses. Trigger keywords: data pipeline, ETL, ELT,
      Kafka, streaming, data warehouse, dbt, Airflow, orchestration, data
      quality, lakehouse, Iceberg, Delta, CDC, Parquet, backfill, watermark,
      Spark, DuckDB, data contract, medallion, dimensional model.
    ---
    
    # SOTA Data Engineering
    
    Expert rules for analytical data systems: pipelines, streaming, warehousing,
    lakehouse storage, data quality, and operations. OLTP schema/query/index craft
    is owned by `sota-databases` — reference it, do not duplicate it. Outbox and
    event-driven service patterns live in `sota-architecture`; backpressure
    mechanics in `sota-async-concurrency`; PII handling in
    `sota-privacy-compliance` (or `sota-code-security` if absent).
    
    Two modes. Pick by intent, then load only the `rules/` files the task needs.
    
    ## BUILD mode
    
    Use when designing or implementing pipelines, models, streaming jobs, or
    storage layouts.
    
    1. **Size the problem first.** Read `rules/01-architecture-and-modeling.md`
       before choosing tools. Most "big data" is small data; DuckDB/Polars on one
       node before a distributed engine. ELT into a warehouse/lakehouse is the
       default shape, not a decision to revisit per pipeline.
    2. **Idempotency is the prime directive.** Every pipeline you write must be
       safe to rerun for any interval at any time. No blind appends. Design the
       write strategy (partition overwrite / merge key / insert-overwrite) before
       the transform logic (`rules/02-pipelines-and-orchestration.md`).
    3. **Batch unless a consumer needs sub-minute data.** Justify streaming in
       writing before building it (`rules/03-streaming-and-cdc.md`).
    4. **Quality checks ship with the pipeline, not after.** Every new model gets
       freshness, volume, uniqueness, and not-null checks tiered block/warn
       (`rules/04-data-quality-and-contracts.md`).
    5. **Decide the physical layout when you create the table.** Partitioning,
       clustering/sort, file sizing, and the maintenance job are part of the
       table's definition (`rules/05-storage-and-performance.md`).
    6. **Ship with operability.** Dev/prod isolation, write-audit-publish for
       risky changes, freshness alerting, a runbook entry
       (`rules/06-operations-and-governance.md`).
    
    ## AUDIT mode
    
    Use when reviewing an existing pipeline repo, dbt project, streaming topology,
    or warehouse.
    
    Procedure:
    1. Inventory: orchestrator + scheduler config, transformation tool (dbt or
       other), storage/table formats, streaming components, quality tooling,
       environments. Read the actual DAGs/models — never audit from README claims.
    2. Load the rules files matching what exists (no Kafka → skip 03).
    3. Verify every finding against real code/config/SQL. Confirm a non-idempotent
       write by reading the write statement, not by inferring from naming.
    4. Report findings in the format below, ordered by severity.
    
    Severity conventions:
    - **CRITICAL** — data corruption or silent wrongness: non-idempotent writes
      that double-count on retry, reruns that duplicate or lose data, CDC deletes
      not applied, PII landing in unprotected zones, prod credentials in dev.
    - **HIGH** — likely incident or unbounded cost: no failure alerting on
      business-critical pipelines, unbounded retries, full-table rescans of large
      sources each run, no backfill path, schema changes that break consumers.
    - **MEDIUM** — erodes trust/efficiency: missing quality checks, `SELECT *`
      staging, small-files accumulation, no documentation/lineage, warn-tier
      checks failing for weeks.
    - **LOW** — hygiene: naming inconsistency, missing column descriptions,
      suboptimal compression.
    
    Finding format:
    ```
    [SEVERITY] <one-line title>
    Where: <file:line / model / DAG / topic>
    Evidence: <the actual code/config/SQL that proves it>
    Impact: <what goes wrong, when>
    Fix: <concrete change, smallest safe diff>
    ```
    
    ## Rules index
    
    | File | Read this when... |
    |---|---|
    | `rules/01-architecture-and-modeling.md` | Choosing engines/architecture (warehouse vs lakehouse vs DuckDB), designing layers (staging/core/mart), dimensional modeling, SCDs, One Big Table, semantic layers, evaluating a data-mesh pitch. |
    | `rules/02-pipelines-and-orchestration.md` | Writing or reviewing any batch pipeline: idempotency, incremental loads, watermarks, late data, backfills, Airflow/orchestrator DAG design, dbt project discipline, scheduling strategy. |
    | `rules/03-streaming-and-cdc.md` | Anything Kafka/Flink/CDC: deciding streaming vs micro-batch, partition keys, consumer groups, offsets, exactly-once claims, schema registry, Debezium, tombstones, DLQs, windowing. |
    | `rules/04-data-quality-and-contracts.md` | Defining data contracts, adding expectation tests, tiering checks block vs warn, drift/anomaly detection, lineage, responding to a data incident, testing transforms in CI. |
    | `rules/05-storage-and-performance.md` | Creating tables, Parquet tuning, partitioning vs clustering, small-files/compaction, Iceberg/Delta features and maintenance, compression, reducing scan cost / warehouse spend. |
    | `rules/06-operations-and-governance.md` | Environments and deployment of pipeline changes, write-audit-publish, blue-green tables, access control, GDPR deletion in lakehouses, pipeline observability, runbooks. |
    
    ## Top 10 non-negotiables
    
    1. **Every pipeline is idempotent.** Rerunning any task for any interval
       produces the same result. Overwrite partitions or MERGE on keys; a blind
       `INSERT INTO ... SELECT` in a scheduled job is a CRITICAL finding.
    2. **No silent failure.** Business-critical pipelines have failure AND
       freshness alerts routed to an owner. A pipeline that fails quietly is
       worse than one that doesn't exist — people keep trusting its output.
    3. **Right-size the engine.** Under ~100 GB working set, single-node
       DuckDB/Polars beats a cluster on cost, speed, and ops. Spark/distributed
       engines need a stated reason (data volume, existing platform, ML scale).
    4. **Incremental by watermark, never by `NOW()` arithmetic in the task.**
       Process data by the orchestrator-supplied logical interval; reruns and
       backfills must produce identical results regardless of wall-clock time.
    5. **Schema changes are backward-compatible or coordinated.** Add columns
       freely; never rename, retype, or drop in place. Producers that break
       consumers without a contract bump are HIGH findings.
    6. **Quality checks are tiered.** Block (fail the pipeline, stop downstream)
       for uniqueness/null/contract violations on critical models; warn (alert,
       continue) for distribution drift. Everything-blocks and nothing-blocks are
       both failure modes.
    7. **Streaming requires a written justification.** Name the consumer that
       needs sub-minute latency. Hourly micro-batch covers most "real-time"
       requests at a tenth of the operational cost.
    8. **Exactly-once is end-to-end or it's a lie.** Kafka transactions cover
       Kafka; your sink makes it true via idempotent writes (merge keys,
       deterministic IDs). Audit the sink, not the producer config.
    9. **No `SELECT *` across layer boundaries.** Staging models enumerate,
       rename, and type columns. Upstream schema drift must break loudly in your
       staging layer, not silently in a dashboard.
    10. **Tables get maintenance from day one.** Compaction, snapshot/manifest
        cleanup, and retention jobs are part of creating an Iceberg/Delta table.
        A lakehouse without maintenance jobs is a slow-motion outage.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related