Skip to content
FREIGHTFLOW / PHASE 01 · ANALYTICS ENGINEERING

Building a Freight Data Platform
with Databricks, dbt & Airflow

PySpark ingests PostgreSQL data into Delta tables. dbt organizes source-facing transformations and analytical marts. Airflow coordinates the batch workflow through Astronomer Cosmos.

PostgreSQLPySparkDatabricksDelta Lakedbt CoreSQL / JinjaAirflowAstronomer CosmosGitHub Actions
14Source tables
4Fact models
7Dimension models
2dbt snapshots
01

Platform overview

End-to-end architecture, layer mapping, orchestration and lineage.

Platform data flow, transformation roles and supporting systems.
Platform data flow, transformation roles and supporting systems.

Why combine Medallion and dbt layers?

PerspectiveQuestion answeredFreightFlow implementation
Medallion
Bronze / Silver / Gold
How far has data been processed and refined?Delta Bronze → logical Silver transformations → Gold analytical marts.
dbt model structure
Staging / Intermediate / Marts
What responsibility does each SQL model have?Source-facing views → reusable transformation tables → facts, dimensions and aggregate.
Deployment targets
dev / ci / prod
Which catalog receives a build or validation run?freight_dev / freight_ci / freight_prod configured in dbt profiles.
These are overlapping perspectives on one pipeline. The repository has schemas named bronze, staging, intermediate, marts and observability; separate physical silver and gold schemas are not required.

Why Medallion?

It communicates the platform-level refinement path, including ingestion outside dbt. It does not prescribe which code framework must implement each step.

Why dbt model layers?

They separate source normalization, reusable SQL logic and published analytical entities. dbt manages dependencies through source() and ref().

Trace one delivery event

oltp.delivery_events→bronze.delivery_events→stg_freight__delivery_events→int_delivery_events_cleaned→fct_delivery_event

The staged model converts a source flag to boolean. The intermediate model recomputes delivery status and tracks mismatches. The fact model adds date and facility keys.

Explore: staging SQL · intermediate SQL · Gold fact SQL

02

Bronze · Ingestion engineering

PostgreSQL → Databricks PySpark → Delta Bronze.

Bronze flow and source-specific load strategies.
Bronze flow and source-specific load strategies.
Input

PostgreSQL / oltp

Six reference tables and eight transaction or metric tables.

Execution

PySpark + JDBC

Databricks notebook constructs source queries and reads them through Spark JDBC.

Output

Delta Bronze

Tables at {catalog}.bronze.{table}, with ingestion metadata.

Loading policy

Overwrite — 6 tables

customers, trucks, trailers, facilities, routes, drivers

Replaces the destination with the current source result.

Watermark — 8 tables

loads, trips, delivery_events, fuel_purchases, maintenance_records, safety_incidents, driver_monthly_metrics, truck_utilization_metrics

Queries rows newer than MAX(target timestamp); appends to existing Delta tables.

Implementation & decisions

Inspect when needed
Actual Spark JDBC ingestion pattern
databricks/nb_ingest_bronze.py · lines 37–50
    if where_clause:
        query += f" WHERE {where_clause}"
    query += f") AS {table}"

    return (
        spark.read.format("jdbc")
        .option("url", jdbc_url())
        .option("dbtable", query)
        .option("user", ghost_user)
        .option("password", ghost_password)
        .option("driver", "org.postgresql.Driver")
        .load()
    )

Open complete file

databricks/nb_ingest_bronze.py · lines 74–86

    df = (
        df.withColumn("_ingested_at", F.current_timestamp())
        .withColumn("_source_file", F.lit(f"ghost_postgres.oltp.{table}"))
    )

    target = f"{target_catalog}.bronze.{table}"
    df.write.format("delta").mode(write_mode).option("mergeSchema", "true").saveAsTable(target)

    print(f"table={table} mode={write_mode} rows_read={rows_read}")


for t in ALL_TABLES:

Open complete file

Why JDBC, overwrite and watermark append?
  • JDBC: a direct batch route from the existing PostgreSQL source.
  • Overwrite: a simple current-state refresh for relatively small reference entities.
  • Watermark append: reads rows with a source timestamp later than the latest loaded timestamp.

Trade-off: a watermark is not CDC. Corrections to older records, deletions and equal-timestamp arrivals may require reconciliation or lookback + merge.

Validation scope and failure modes
  • The notebook logs rows_read and adds _ingested_at and a logical _source_file identifier.
  • Delta writes configure mergeSchema=true; this does not constitute a data contract.
  • The submitted source defines the write logic; ingestion throughput, retry idempotency and late-update recovery are not benchmarked here.

Source: databricks/nb_ingest_bronze.py

03

Silver · dbt transformations

Staging prepares source-aligned views; intermediate models apply reusable SQL logic.

Silver model responsibilities and the separate exception-tracking branch.
Silver model responsibilities and the separate exception-tracking branch.
Staging · source-aligned

14 dbt views

  • Read declared Bronze relations with source().
  • Apply minimal type normalization and consistent source-facing names.
  • Example: string-to-boolean conversion for on_time_flag.
Materialization: view in dbt_project.yml.
Intermediate · reusable logic

Tables + exception models

  • Calculate timestamp-based delivery status and mismatch flags.
  • Enrich load records before marts.
  • Track timestamp anomalies, flag mismatches and orphan references.
Main reviewed intermediate models explicitly set table.

Why two dbt layers inside Silver?

Staging isolates source-facing normalization. Intermediate carries reusable business logic and joins. Keeping these responsibilities separate gives marts cleaner inputs and makes the transformations easier to test independently. Smaller projects can omit intermediate when it adds no useful boundary.

Staging: SQL example and source configuration
dbt/models/staging/stg_freight__delivery_events.sql · lines 1–8
select
    {{ dbt_utils.star(source('bronze', 'delivery_events'), except=["on_time_flag"]) }},
    case
        when lower(on_time_flag) = 'true' then true
        when lower(on_time_flag) = 'false' then false
    end as on_time_flag
from {{ source('bronze', 'delivery_events') }}

Open complete file

Staging views avoid persisting lightweight projections. The trade-off is repeated SQL evaluation when downstream models run.

Source declarations and freshness thresholds are configured in dbt/models/staging/_staging__sources.yml.

Intermediate: SQL example and reusable macro
dbt/models/intermediate/int_delivery_events_cleaned.sql · lines 1–24
{{ config(materialized="table", tags=["intermediate"], event_time="actual_datetime") }}

select
    event_id,
    load_id,
    trip_id,
    event_type,
    facility_id,
    scheduled_datetime,
    actual_datetime,
    detention_minutes,
    on_time_flag as on_time_flag_raw,
    {{ recompute_on_time("actual_datetime", "scheduled_datetime") }}
        as is_on_time_computed,
    on_time_flag
    <> {{ recompute_on_time("actual_datetime", "scheduled_datetime") }}
        as flag_mismatch,
    location_city,
    location_state
from {{ ref("stg_freight__delivery_events") }}

Open complete file

The model preserves the original flag and adds a computed flag and mismatch indicator. It calls a reusable dbt macro.

Macro: dbt/macros/business/recompute_on_time.sql

Exception tracking and downstream boundaries

The following models record anomalies for inspection:

They do not automatically prevent every flagged record from reaching all Gold facts. An exclusion policy must be implemented in downstream SQL or a publication gate.

Folder: dbt/models/intermediate/

04

Gold · Data model & marts

Four facts, seven dimensions, one aggregate and separate historical snapshots.

Gold relationship evidence: joins drawn from the four fact SQL models, with dim_trailer deliberately unconnected.
Gold relationship evidence: joins drawn from the four fact SQL models, with dim_trailer deliberately unconnected.

The Gold layer publishes four fact models, seven dimensions and a daily aggregate. Each fact has a defined intended grain; production grain uniqueness still requires data tests and runtime evidence.

Fact modelIntended grainActual direct dimension joinsMaterialization
fct_load1 row / loadcustomer, route, dateIncremental merge
fct_delivery_event1 row / eventfacility, dateIncremental monthly microbatch
fct_fuel_purchase1 row / purchasetruck, driver, dateIncremental append
fct_shipment_lifecycle1 row / load lifecycleNone (direct Gold joins)Incremental merge + contract

Dimension models

dim_datedim_customerdim_routedim_facilitydim_driverdim_truckdim_trailer

The seventh dimension, dim_trailer, has no direct FK join from these four fact SQL selections.

Why dimensional modeling?

Grain-specific facts support defined event and transaction measures; conformed dimensions organize reusable descriptors for analysis. Join cardinality must be checked to protect aggregations.

Historical attributes

dbt snapshots track selected driver and truck changes using the check strategy. Current mart dimensions still read staging; historical as-of joins are not implemented in these marts.

Merge, microbatch and append: implementation comparison
  • Merge / fct_load: configured with unique_key=load_id; updates intended load-level rows.
  • Microbatch / fct_delivery_event: configured by actual_datetime in monthly batches.
  • Append / fct_fuel_purchase: uses a max purchase-date filter; corrected older rows may not be reprocessed.
  • Materialized view / agg_daily_ontime: daily delivery aggregates.

Validation boundary: incremental keys and contracts do not independently prove fact grain uniqueness.

dbt/models/marts/facts/fct_load.sql · lines 1–12
{{
    config(
        materialized="incremental",
        incremental_strategy="merge",
        unique_key="load_id",
        on_schema_change="append_new_columns",
        partition_by=["load_month"],
        tags=["marts"],
    )
}}

select

Open complete file

Grain risk in fct_shipment_lifecycle

Several CTEs join on load_id. If trips, pickup events or delivery events contain multiple rows per load, the join can multiply lifecycle rows. A unique_key setting does not deduplicate those upstream records automatically.

Consider windowed selection, aggregate-by-grain or explicit uniqueness tests when improving the model.

dbt/models/marts/facts/fct_shipment_lifecycle.sql · lines 12–47
with
pickup_events as (
    select
        load_id,
        scheduled_datetime as pickup_scheduled,
        actual_datetime as picked_up_at
    from {{ ref("int_delivery_events_cleaned") }}
    where event_type = 'Pickup'
),

delivery_events as (
    select
        load_id,
        scheduled_datetime as delivery_scheduled,
        actual_datetime as delivered_at
    from {{ ref("int_delivery_events_cleaned") }}
    where event_type = 'Delivery'
),

trip_dispatch as (
    select
        load_id,
        dispatch_date as dispatched_at,
        trip_id
    from {{ ref("stg_freight__trips") }}
),

trip_fuel_cost as (
    select
        t.load_id,
        sum(fp.total_cost) as fuel_cost
    from {{ ref("stg_freight__trips") }} as t
    inner join {{ ref("stg_freight__fuel_purchases") }} as fp on t.trip_id = fp.trip_id
    group by 1
)

Open complete file

Snapshot and contract code
dbt/snapshots/snap_drivers.sql · lines 1–14
{% snapshot snap_drivers %}
    {{
        config(
            target_schema="marts",
            unique_key="driver_id",
            strategy="check",
            check_cols=["employment_status", "termination_date", "home_terminal"],
            hard_deletes="new_record",
        )
    }}
    select *
    from {{ ref("stg_freight__drivers") }}
{% endsnapshot %}

Open complete file

The lifecycle contract is declared here: dbt/models/marts/_marts__models.yml. The Gold aggregate definition is dbt/models/marts/agg_daily_ontime.sql.

05

Inside the dbt project

16 repository components, from source() and ref() to snapshots, tests and hooks.

This section explains dbt mechanisms used across Silver and Gold. Expand any component to inspect its responsibility, design rationale and an excerpt from the submitted repository.

01 · ConfigurationProject configuration
Controls model paths, schemas, materializations, access and on-run-end hooks.

Reason: A single project config makes model placement and build behavior inspectable.

dbt/dbt_project.yml · lines 20–48
clean-targets: # directories to be removed by `dbt clean`
  - "target"
  - "dbt_packages"

# Configuring models
# Full documentation: https://docs.getdbt.com/docs/configuring-models

# In this example config, we tell dbt to build all models in the example/
# directory as views. These settings can be overridden in the individual model
# files using the `{{ config(...) }}` macro.
models:
  freight:
    +persist_docs:
      # Push model description as a database table comment
      relation: true
      # Push column descriptions as database column comments
      columns: true
    staging:
      +schema: staging
      +materialized: view
    observability:
      +schema: observability
    intermediate:
      +schema: intermediate
      +group: core
      +access: protected
    marts:
      +schema: marts
      +group: core

Open complete file

02 · LineageSource definitions
Declares Bronze sources in the active catalog; freshness thresholds are configured.

Reason: source() references a declared external relation.

dbt/models/staging/_staging__sources.yml · lines 1–27
version: 2

sources:
  - name: bronze
    database: "{{ target.catalog }}"
    schema: bronze
    tables:
      - name: customers
        config:
          loaded_at_field: _ingested_at
      - name: trucks
        config:
          loaded_at_field: _ingested_at
      - name: trailers
        config:
          loaded_at_field: _ingested_at
      - name: facilities
        config:
          loaded_at_field: _ingested_at
      - name: routes
        config:
          loaded_at_field: _ingested_at
      - name: drivers
        config:
          loaded_at_field: _ingested_at
      - name: loads
        config:

Open complete file

03 · Lineageref() & model DAG
Connects staging to reusable intermediate transformations.

Reason: dbt resolves dependencies and model locations through ref().

dbt/models/intermediate/int_delivery_events_cleaned.sql · lines 1–21
{{ config(materialized="table", tags=["intermediate"], event_time="actual_datetime") }}

select
    event_id,
    load_id,
    trip_id,
    event_type,
    facility_id,
    scheduled_datetime,
    actual_datetime,
    detention_minutes,
    on_time_flag as on_time_flag_raw,
    {{ recompute_on_time("actual_datetime", "scheduled_datetime") }}
        as is_on_time_computed,
    on_time_flag
    <> {{ recompute_on_time("actual_datetime", "scheduled_datetime") }}
        as flag_mismatch,
    location_city,
    location_state
from {{ ref("stg_freight__delivery_events") }}

Open complete file

04 · TransformsMaterializations
Staging views, intermediate tables, incremental facts and one materialized-view aggregate.

Reason: Selection changes compute timing, storage and update behavior.

dbt/models/marts/facts/fct_delivery_event.sql · lines 1–13
{{
    config(
        materialized="incremental",
        incremental_strategy="microbatch",
        event_time="actual_datetime",
        batch_size="month",
        begin="2022-01-01",
        on_schema_change="append_new_columns",
        tags=["marts"],
    )
}}

select

Open complete file

05 · TransformsMacros & Jinja
Centralizes the timestamp-based on-time expression.

Reason: Reused SQL expressions can be tested without copying logic to every model.

dbt/macros/business/recompute_on_time.sql · lines 1–18
{% macro recompute_on_time(actual_col, scheduled_col) %}
    case
        when {{ actual_col }} is null or {{ scheduled_col }} is null
        then null
        when {{ actual_col }} <= {{ scheduled_col }}
        then true
        else false
    end
{% endmacro %}

Open complete file

06 · QualityGeneric tests
Contains a custom relationship-quality test with a threshold.

Reason: Generic tests parameterize checks across model columns.

dbt/macros/generic/test_not_orphaned.sql · lines 1–20
{% test not_orphaned(model, column_name, to, field, threshold_pct=0) %}
    with
        orphans as (
            select {{ column_name }} as key_value
            from {{ model }}
            where
                {{ column_name }} is not null
                and {{ column_name }} not in (select {{ field }} from {{ to }})
        ),
        counts as (
            select
                (select count(*) from orphans) as orphan_count,
                (select count(*) from {{ model }}) as total_count
        )
    select o.*
    from orphans o
    cross join counts c
    where c.orphan_count > c.total_count * {{ threshold_pct }} / 100.0
{% endtest %}

Open complete file

07 · QualitySingular tests
Defines a query that returns records violating an assertion.

Reason: Keeps checks for specific SQL behavior in version control.

dbt/tests/assert_on_time_flag_matches_computed.sql · lines 1–19
-- depends_on: {{ ref('qtn_flag_mismatch') }}
select c.event_id
from {{ ref("int_delivery_events_cleaned") }} c
left join {{ ref("qtn_flag_mismatch") }} q on c.event_id = q.event_id
where c.flag_mismatch = true and q.event_id is null

Open complete file

08 · QualityUnit tests
Specifies controlled inputs and expected outputs for transformation logic.

Reason: Tests a model transformation independently of the complete dataset.

dbt/models/intermediate/_unit_tests.yml · lines 1–25
unit_tests:
  - name: test_recompute_on_time_logic
    model: int_delivery_events_cleaned
    given:
      - input: ref('stg_freight__delivery_events')
        rows:
          - {
              event_id: E1,
              load_id: L1,
              trip_id: T1,
              event_type: Pickup,
              facility_id: F1,
              scheduled_datetime: "2024-01-01 10:00:00",
              actual_datetime: "2024-01-01 09:00:00",
              detention_minutes: 0,
              on_time_flag: false,
              location_city: Dallas,
              location_state: TX,
            }
          - {
              event_id: E2,
              load_id: L2,
              trip_id: T2,
              event_type: Delivery,
              facility_id: F2,

Open complete file

09 · QualitySeeds
CSV rules metadata read by the quality-rule builder.

Reason: Treats rule definitions as versioned project inputs.

dbt/seeds/dq_rules.csv · lines 1–5
rule_id,model_name,column_name,rule_type,rule_expression,threshold_pct,severity,owner,description
DQ001,stg_freight__loads,load_id,not_null,,0,error,data-team,Primary key must not be null
DQ002,stg_freight__trips,driver_id,not_orphaned,stg_freight__drivers.driver_id,2,warn,data-team,Trip references a driver_id not present in drivers
DQ003,stg_freight__trips,truck_id,not_orphaned,stg_freight__trucks.truck_id,2,warn,data-team,Trip references a truck_id not present in trucks
DQ004,stg_freight__fuel_purchases,gallons,in_range,0:300,0.1,warn,finance,Fuel volume outside plausible range

Open complete file

10 · LifecycleSnapshots
check strategy tracks changes to selected driver attributes.

Reason: Snapshot history is separate from current-state driver dimension.

dbt/snapshots/snap_drivers.sql · lines 1–15
{% snapshot snap_drivers %}
    {{
        config(
            target_schema="marts",
            unique_key="driver_id",
            strategy="check",
            check_cols=["employment_status", "termination_date", "home_terminal"],
            hard_deletes="new_record",
        )
    }}
    select *
    from {{ ref("stg_freight__drivers") }}
{% endsnapshot %}

Open complete file

11 · OperationsObservability
Computes rule results and failure percentages in an observability model.

Reason: Profile and rule results support inspection; not automatically a Gold filter.

dbt/models/observability/dq_rule_results.sql · lines 1–20
-- depends_on: {{ ref('stg_freight__loads') }}
-- depends_on: {{ ref('stg_freight__trips') }}
-- depends_on: {{ ref('stg_freight__drivers') }}
-- depends_on: {{ ref('stg_freight__trucks') }}
-- depends_on: {{ ref('stg_freight__fuel_purchases') }}
-- depends_on: {{ ref('stg_freight__delivery_events') }}
{{ config(materialized="incremental", tags=["dq"]) }}

with rule_checks as ({{ build_dq_sql() }})

select
    *,
    round(100.0 * failed_rows / nullif(total_rows, 0), 2) as failed_pct,
    (100.0 * failed_rows / nullif(total_rows, 0)) <= threshold_pct as is_passed
from rule_checks

Open complete file

12 · OperationsEnd-of-run hooks
Checks quality outcomes after dbt has run selected resources.

Reason: A hook after execution cannot retroactively prevent earlier table writes.

dbt/macros/dq/dq_severity_gate.sql · lines 1–24
{% macro dq_severity_gate() %}
    -- depends_on: {{ ref('dq_rule_results') }}
    {% if execute %}
        {# Check if dq_rule_results exists before querying to prevent error on initial tasks #}
        {%- set relation = adapter.get_relation(
            database=(
                target.catalog
                if target.type == "databricks"
                else target.database
            ),
            schema=generate_schema_name("observability"),
            identifier="dq_rule_results",
        ) -%}

        {% if relation is not none %}
            {%- set check_query -%}
      select count(*) as n
      from {{ ref('dq_rule_results') }}
      where severity = 'error' and not is_passed
        and dbt_invocation_id = '{{ invocation_id }}'
            {%- endset -%}

            {%- set results = run_query(check_query) -%}
            {%- set failed_count = results.columns[0].values()[0] if results else 0 -%}

Open complete file

13 · QualityContracts
Enforced contract is defined for fct_shipment_lifecycle.

Reason: Schema/type contracts and row-level tests solve different problems.

dbt/models/marts/_marts__models.yml · lines 1–24
version: 2

models:
  - name: fct_shipment_lifecycle
    config:
      contract:
        enforced: true
    columns:
      - name: load_id
        data_type: string
        constraints:
          - type: not_null
          - type: primary_key
      - name: booked_at
        data_type: timestamp
      - name: dispatched_at
        data_type: timestamp
      - name: picked_up_at
        data_type: timestamp
      - name: delivered_at
        data_type: timestamp
      - name: hrs_book_to_dispatch
        data_type: double
      - name: hrs_dispatch_to_pickup

Open complete file

14 · MetadataExposures & docs
Declares downstream consumers in the dbt manifest.

Reason: Exposure metadata represents a declared relationship, not live proof of integration.

dbt/models/marts/_marts__exposures.yml · lines 1–27
version: 2

exposures:
  - name: freight_sla_dashboard
    type: dashboard
    maturity: high
    url: "{{ var('sla_dashboard_url', 'https://replace-with-real-url') }}"
    description: >
      SLA dashboard used by the operations team — on-time rate by route/customer/driver,
      cost-per-mile, data quality scorecard.
    depends_on:
      - ref('fct_shipment_lifecycle')
      - ref('fct_load')
      - ref('dim_customer')
      - ref('dim_route')
    owner:
      name: Huu Toan
      email: toandata.work@gmail.com

Open complete file

15 · ConfigurationSchema routing
Overrides default schema naming to use the configured custom schema.

Reason: Changes the physical destination of dbt models.

dbt/macros/generate_schema_name.sql · lines 1–10
{% macro generate_schema_name(custom_schema_name, node) -%}
    {%- set default_schema = target.schema -%}
    {%- if custom_schema_name is none -%}
        {{ default_schema }}
    {%- else -%}
        {{ custom_schema_name | trim }}
    {%- endif -%}
{%- endmacro %}

Open complete file

16 · ConfigurationGroups & access
Defines project ownership grouping used by model access configuration.

Reason: dbt model access does not establish cloud IAM enforcement.

dbt/models/groups.yml · lines 1–22
groups:
  - name: core
    owner:
      name: core

Open complete file

Configured test definitions, freshness thresholds and hooks are code evidence. They do not establish a successful full-data test execution unless corresponding run artifacts are supplied.
06

Orchestration, environments & CI/CD

Scheduling, dbt-aware task graphs, validation workflows and deployment targets.

Airflow/Cosmos runtime flow, separately configured dbt catalogs, and GitHub Actions workflows.
Airflow/Cosmos runtime flow, separately configured dbt catalogs, and GitHub Actions workflows.

Daily orchestration

  • freight_daily is scheduled at 0 6 * * * in scheduler timezone.
  • Airflow triggers a Databricks ingestion job and waits for it.
  • Cosmos builds the dbt_freight task group and dbt dependencies.
  • Its config sets TestBehavior.AFTER_ALL.

Environment targets

  • dev → freight_dev
  • ci → freight_ci
  • prod → freight_prod

Catalog configuration alone does not prove compute, network or IAM isolation.

Airflow failure behavior: inspect code
airflow/dags/freight_daily.py · lines 39–82
        Includes a graceful fallback for local/dev environments when cluster quota 
        or workspace job trigger is temporarily unavailable.
        """
        import logging
        from databricks.sdk import WorkspaceClient

        logger = logging.getLogger("airflow.task")
        strict_mode = os.getenv("AIRFLOW_STRICT_INGESTION", "false").lower() == "true"

        try:
            w = WorkspaceClient(
                host=os.environ["DBX_HOST"],
                token=os.environ["DBX_TOKEN"]
            )
            logger.info(f"Triggering Databricks ingestion job targeting catalog: {TARGET_CATALOG}")
            run = w.jobs.run_now(
                job_id=int(os.environ["DBX_JOB_ID"]),
                notebook_params={"target_catalog": TARGET_CATALOG}
            )
            w.jobs.wait_get_run_job_terminated_or_skipped(run_id=run.run_id)
            logger.info("Databricks Ingestion Job completed successfully.")
        except Exception as e:
            if strict_mode:
                logger.error(f"Critical: Ingestion failed in strict mode: {e}")
                raise e
            
            logger.warning(
                f"Databricks job trigger unavailable ({e}). "
                f"Dev mode active: proceeding downstream with existing {TARGET_CATALOG} Bronze Delta tables."
            )

    dbt_tg = DbtTaskGroup(
        group_id="dbt_freight",
        project_config=project_config,
        profile_config=profile_config,
        execution_config=execution_config,
        render_config=RenderConfig(test_behavior=TestBehavior.AFTER_ALL),
    )

    ingest_bronze() >> dbt_tg


freight_daily()

Open complete file

With AIRFLOW_STRICT_INGESTION=false (default), a job-trigger failure can be logged while downstream dbt continues using prior Bronze tables. Strict mode raises the error.

CI / CD and backfill boundaries
TriggerDefined workflowBoundary
PR to mainSQLFluff lint; dbt build --select marts --empty --target ciNot a full-data integration run
Push to maindbt docs generate + GitHub Pages deploymentDocs publishing, not production data promotion
Manual backfillfreight_backfill selects fct_delivery_event on prod targetRepeated-run correctness not shown

CI workflow · docs workflow · backfill DAG

Runtime code: airflow/dags/freight_daily.py · dbt/profiles.yml

07

Execution evidence & decisions

Reviewable implementation proof, design consequences and known validation boundaries.

Original Airflow task graph screenshot supplied with repository
Original Airflow screenshot; click to inspect visible task states.

What the screenshot supports

  • The named DAG and its task graph were visible in Airflow.
  • Historical states include both successful and failed/unfinished tasks.
  • It captures a moment in run history.

What it does not establish

  • Production SLA or uptime.
  • Successful full-data test history.
  • Throughput, freshness or operating-cost results.

Design decisions and their consequences

Decision in codeReason it can be usefulEngineering consequence
Direct Spark JDBCShort batch ingestion pathNo CDC change-log capture
Staging viewsLow duplication for simple SQL normalizationView work may repeat downstream
Intermediate tablesPersist reused derived logicExtra write/storage overhead
Mixed fact incrementalsTailor writes to model grain/timeDifferent recovery and late-update risks
dbt snapshotsRetain selected attribute change historyCurrent mart dimensions do not use historical snapshots
on-run-end DQ hookCheck rule results after dbt invocationDoes not retroactively prevent mart writes

Evidence categories: code-defined behavior · documented profiling · captured execution. No performance or runtime reliability benchmark is claimed.

08

Source code & documentation

Direct links to the repository, model definitions and supporting implementation notes.

The source repository is the technical reference for the claims on this page. Links below open the corresponding implementation files.