Skip to content

DS 301 · Module 2

Reliable Data Products: Validation, Delivery, Observability, and Recovery

Learn safe scheduling and incremental runs, full logging, metrics, lineage, and observability, human approval and rollback before publishing, and testing, failure recovery, and runbooks, then deliver a reliable analytics workflow in an Independent Lab.

Start Section 1
0 / 5 completed
Sync my progressJoin a session to keep progress across devices and share it with your instructor

Info

After you join, your completed lesson numbers are sent to your instructor together with your learner code, so they can see your progress. Use the codes your instructor gave you, and do not enter your name, company secrets or other personal data. Progress is kept with the rest of that session's data.

Learning objectives

  • Design reruns safely; understand full refresh vs incremental processing
  • Capture run IDs, row counts, data freshness, validation results, failures, lineage
  • Place approval before high-impact publish; preserve prior artifacts; define rollback
  • Use unit/data/integration smoke tests; define retries, dead-letter/manual intervention, runbook
  • Deliver executable prototype, architecture canvas, tests, observability plan, runbook, and ownership model

Scheduling, Idempotency, and Incremental Runs

45 min

Scheduling, Idempotency, and Incremental Runs

Learning objective: design reruns safely; understand full refresh vs incremental processing.

A scheduled run kicks off at 2am and fails halfway through because of a network blip. The on-call engineer manually retriggers it, and the next morning you discover the same batch_id was counted twice in the output table, doubling the total. This is a classic non-idempotent pipeline: rerunning the same input did not overwrite or replace the previous result, it stacked on top of it. Designing safe reruns is a baseline requirement for any scheduled pipeline, not an advanced option.

From "append" to an idempotent write
# Wrong: every run appends, so a rerun creates duplicate rows
result.to_sql("monthly_summary", con=engine, if_exists="append")

# Right: key the write by the run's processing range, delete the same range's old
# result first, then write the new one (idempotent)
def publish_idempotent(result: pd.DataFrame, period: str, engine) -> None:
    with engine.begin() as conn:
        conn.execute(
            "DELETE FROM monthly_summary WHERE period = :period",
            {"period": period}
        )
        result.to_sql("monthly_summary", con=conn, if_exists="append", index=False)

# An incremental run needs to track how far it has already processed (a watermark)
def get_watermark(engine) -> str:
    row = engine.execute("SELECT MAX(period) AS last_period FROM monthly_summary").first()
    return row.last_period if row and row.last_period else "1970-01"
Full refresh vs incremental processing trade-offs
ApproachGood fitRisk
Full refreshSmall data volumes; source occasionally revises historical dataRuntime and cost grow as data volume grows
IncrementalLarge, stable data volumes that only ever appendMust track a watermark correctly and handle late-arriving data
Another idempotent pattern: upsert by key

Besides "delete the same period then write," another common pattern is to upsert on a key such as (source_system, period, metric): update the row if the key already exists, insert it if not. Both approaches keep the output table consistent no matter how many times the same input batch is rerun; which one to use depends on whether the database makes upsert syntax convenient and whether a recompute always covers a whole period at once.

  1. Check whether the write is idempotent
    Rerunning the same input twice should leave the output table exactly the same, never doubled.
  2. Choose full refresh or incremental
    Decide based on data volume and whether the source revises history, whether to recompute everything or just the new part.
  3. Track a watermark
    An incremental pipeline needs to record how far it has already processed, as the starting point for the next run.
  4. Handle late-arriving data
    Design a mechanism so data that arrives later than expected is still correctly attributed to its period.

Warning

"The schedule ran, and nothing errored" does not mean the result is correct. The most common symptom of a non-idempotent pipeline is a number quietly doubling or shrinking after a rerun or an overlapping scheduled run.
Check yourself

A scheduled run fails from a network issue, an on-call engineer manually reruns the same batch, and the output table’s total doubles. What design is this pipeline missing?

A scheduled run fails from a network issue, an on-call engineer manually reruns the same batch, and the output table’s total doubles. What design is this pipeline missing?

Key takeaway: Completing the steps is not enough; the result must be verifiable, reproducible, and explainable.

Logging, Metrics, Lineage, and Observability

45 min

Logging, Metrics, Lineage, and Observability

Learning objective: capture run IDs, row counts, data freshness, validation results, failures, lineage.

Your pipeline shows last night’s scheduled run as "succeeded," but your manager complains the monthly report’s numbers are down 90% from normal. You open the log and see a single line, "Job completed successfully", no row counts, no validation results, nothing to tell you which stage went wrong. Logging only a binary success/failure status is like driving with no dashboard: the car is still moving, but you have no idea how much fuel is left or whether the engine is overheating.

From "only log success/failure" to meaningful observability
import logging, uuid

logger = logging.getLogger("ds301.pipeline")

def run_pipeline(input_path: str) -> None:
    run_id = str(uuid.uuid4())
    logger.info("run_start", extra={"run_id": run_id})

    raw = load_raw(input_path)
    logger.info("ingest_done", extra={"run_id": run_id, "row_count": len(raw)})

    errors = validate_contract(raw)
    logger.info("validation_done", extra={"run_id": run_id, "error_count": len(errors)})

    transformed = fill_missing_values(filter_current_schema(raw, "v2"))
    summary = summarize_by_metric(transformed)
    logger.info(
        "analyze_done",
        extra={"run_id": run_id, "output_row_count": len(summary), "total_value": float(summary["value"].sum())}
    )

    logger.info("run_end", extra={"run_id": run_id, "status": "succeeded"})
What every run should log, at minimum
ItemWhy it matters
run_idTies every log line from the same run together for tracing
Row counts (before/after each stage)A sudden change in data volume is usually the first sign of a problem
Validation resultsWhich contract rules were violated, even if only warnings
FreshnessWhat time period this run’s data actually covers
LineageWhich version of the input data and code produced this output
  • Row count shifts beyond a reasonable threshold compared with the previous run (for example, plus or minus 30%)
  • The validation error count goes from zero to nonzero, even if it is currently only warning-level
  • A given run_id’s freshness falls behind the scheduled expectation by more than a set threshold
  • Lineage records show this run used an unexpected input version or code version

Warning

"Succeeded/failed" only answers "did it crash?" It cannot answer "was this run’s result correct?" Row counts collapsing to a tenth of normal is often more dangerous than an explicit error, precisely because it will not fail the pipeline.
Check yourself

A pipeline’s log only shows "Job completed successfully," with no row counts or validation results. What does this logging approach mainly lack?

A pipeline’s log only shows "Job completed successfully," with no row counts or validation results. What does this logging approach mainly lack?

Key takeaway: Completing the steps is not enough; the result must be verifiable, reproducible, and explainable.

Human Approval, Publishing, and Rollback

45 min

Human Approval, Publishing, and Rollback

Learning objective: place approval before high-impact publish; preserve prior artifacts; define rollback.

Your pipeline is fully automated end to end, publishing straight to an internal dashboard. Last month, an upstream field definition silently changed, and the published monthly metrics undercounted an entire business unit, with no human ever looking at it before it went out, because the flow was fully automatic. By the time a manager spotted the discrepancy in a meeting, the wrong report had already been circulating for two weeks. High-impact data products need a person to confirm the result is reasonable before the "publish to users" step, rather than trusting automation completely.

Deciding whether to add a human approval gate
ScenarioJudgment
An internal intermediate table only engineers look atUsually not needed; auto-publish is fine
A monthly report used externally or for executive decisionsShould have an approval gate; a person confirms before publish
A financial, compliance, or accounting-related data productAlmost always needs approval, with an audit trail
Adding an approval gate and versioned output before publish
def publish_with_approval(result: pd.DataFrame, period: str, approved_by: str | None) -> None:
    if approved_by is None:
        # High-impact output: cannot publish without approval; write to a pending-review area
        write_to_pending_review(result, period)
        raise RuntimeError(f"period={period} not yet approved; written to pending review, awaiting human confirmation")

    # Versioned output: keep an identifiable prior version so rollback is possible
    archive_previous_version(period)
    publish_idempotent(result, period, engine)
    log_publish_event(period, approved_by)

def rollback(period: str) -> None:
    restore_previous_version(period)
    log_rollback_event(period)
  • After publishing, you discover the upstream data had already quietly degraded before publish, but the contract check did not catch it
  • The approver changes their mind afterward and decides this result should not have been published
  • A downstream user reports that the numbers clearly do not match their own records
  • You discover this publish used the wrong code version or configuration

Warning

Rollback’s precondition is that you already preserved an identifiable prior version of the output, plus a concrete way to switch back to it. If every publish simply overwrites the one and only output, there is nothing left to roll back to when something goes wrong.

Info

Human, AI, Human: AI or automation can produce a candidate result, but a high-impact publish still needs a person to confirm "this result can be stood behind publicly", exactly the human review point this course emphasizes.
Check yourself

A monthly report used for executive decisions is fully automated end to end, with no one reviewing it before it publishes. Under this course’s principles, what is the most appropriate improvement?

A monthly report used for executive decisions is fully automated end to end, with no one reviewing it before it publishes. Under this course’s principles, what is the most appropriate improvement?

Key takeaway: Completing the steps is not enough; the result must be verifiable, reproducible, and explainable.

Testing, Failure Recovery, and Runbooks

45 min

Testing, Failure Recovery, and Runbooks

Learning objective: use unit/data/integration smoke tests; define retries, dead-letter/manual intervention, runbook.

At 3am, a pipeline fails because an upstream API times out temporarily. The schedule is configured to retry indefinitely on failure, so it retries and fails all night long, and by morning you find the system has run out of resources, while the real problem (the upstream API recovered hours ago, but the code has a separate, permanent bug) was never actually addressed. A reliable workflow needs three things at once: tests that catch known errors before they happen, a disciplined retry strategy that distinguishes transient from permanent failures, and a runbook so whoever is on call knows what to do when something fails.

A retry strategy that distinguishes transient from permanent errors
import time

class TransientError(Exception): ...
class PermanentError(Exception): ...

def call_upstream_with_retry(fetch_fn, max_attempts: int = 3) -> dict:
    for attempt in range(1, max_attempts + 1):
        try:
            return fetch_fn()
        except TransientError as e:
            if attempt == max_attempts:
                raise
            time.sleep(2 ** attempt)  # exponential backoff so we don't hammer the upstream service
        except PermanentError:
            # Permanent error (e.g. malformed data): retrying will never succeed; log it and hand off to a human
            log_for_manual_intervention(fetch_fn.__name__)
            raise
What each of the three test layers verifies
Test typeWhat it verifies
Unit testWhether a single function’s logic is correct (e.g. an aggregation formula)
Data testWhether the actual incoming data meets the contract and quality expectations
Integration smoke testWhether the whole pipeline runs end to end and the output falls within a reasonable range
A sample runbook entry

Scenario: the pipeline fails at the validate stage with "unknown metric value found." Diagnosis: first confirm whether this is a newly added upstream metric or simply a typo. Response: if it is a typo, notify upstream to fix it and rerun the batch; if it is a genuine new metric, confirm with the data team whether to add it to ALLOWED_METRICS before updating the contract. Escalate to: the data platform owner.

  1. Distinguish error types
    Split errors into transient (worth retrying) and permanent (retrying is pointless).
  2. Set a retry cap
    Avoid unlimited retries that waste resources or stall the entire schedule.
  3. Define when a human steps in
    A permanent error or exceeding the retry cap should notify a person, not keep retrying.
  4. Write a runbook
    Document common failure scenarios, how to diagnose them, response steps, and who to escalate to.

Warning

Unlimited retries are not "extra safe", they turn a clear failure into an invisible problem that keeps consuming resources and never reaches a conclusion.
Check yourself

A pipeline fails from a permanent data-format error (retrying will never fix it), but the schedule is configured to retry indefinitely on failure. What is the problem with this design?

A pipeline fails from a permanent data-format error (retrying will never fix it), but the schedule is configured to retry indefinitely on failure. What is the problem with this design?

Key takeaway: Completing the steps is not enough; the result must be verifiable, reproducible, and explainable.

Independent Lab: Reliable Analytics Workflow

60 min

Independent Lab: Reliable Analytics Workflow

Learning objective: independently deliver an executable prototype, architecture canvas, tests, observability plan, runbook, and ownership model.

Info

This is DS 301’s capstone lab: with no more step-by-step guidance, you need to independently integrate everything you’ve learned into a reliable analytics workflow, and deliver an Analytics Workflow Canvas + Executable Workflow Prototype + Runbook.
  1. Input Contract
    Write down this workflow’s input data contract and the assumptions it allows.
  2. Executable Prototype
    Deliver a runnable pipeline covering the five ingest→validate→transform→analyze→publish stages.
  3. Tests
    Include at least one unit test and one data validation check.
  4. Observability
    Log run IDs, row counts, validation results, and lineage.
  5. Human Approval & Rollback
    Describe the human approval point before publish and how to roll back to a prior version.
  6. Runbook
    Write down how to diagnose, respond to, and escalate at least two common failure scenarios.
  7. Ownership Model
    Explicitly state who owns this data product and what a handoff needs to know.
Minimum content the Independent Lab should include (fill in your own results)
ItemWhat to report
Executable PrototypeThe five stages you delivered and their checkpoints
ObservabilityA sample of the run IDs, row counts, and validation results you logged
RunbookDiagnosis and response steps for at least two failure scenarios
Still needs human judgmentAt least one assumption or decision that still needs business or governance confirmation
  • Only testing the happy path, without verifying the pipeline actually fails when data violates the contract
  • Observability only logs success or failure, without row counts or validation results
  • The runbook is too vague (for example, just "contact an engineer"), with no concrete diagnosis steps or escalation target
  • The ownership model lists only a name, without explaining what background knowledge a handoff actually needs

Warning

"The pipeline runs end to end" does not equal "this is a reliable data product." A pipeline with no tests, observability, or runbook is just a differently shaped one-off notebook.

Done

Before finishing, ask yourself: if this pipeline failed at 3am, could whoever is on call use only your runbook and logs to decide what to do, without waking you up?
Check yourself

In the Independent Lab, you delivered a pipeline that runs cleanly and produces the correct result, but you wrote no runbook and logged no run IDs or row counts. Does this satisfy the course’s final capstone skill?

In the Independent Lab, you delivered a pipeline that runs cleanly and produces the correct result, but you wrote no runbook and logged no run IDs or row counts. Does this satisfy the course’s final capstone skill?

Key takeaway: Completing the steps is not enough; the result must be verifiable, reproducible, and explainable.

What next?