Spatial Data Architecture & Governance

The convergence of cloud-native analytics and location intelligence has rewritten how organizations ingest, transform, and serve geospatial data. A durable spatial data architecture treats geometry, topology, and coordinate systems as first-class citizens inside the analytical stack rather than as secondary artifacts bolted onto tabular pipelines. When geometry is modeled with the same rigor as a fact table, teams gain reproducible transformations, deterministic tests, and lineage that survives schema migrations — the same guarantees analytics engineers already expect from the core fundamentals and architecture for dbt geospatial.

Governance is the discipline that keeps that architecture trustworthy as it scales. It spans coordinate reference system policy, geometry validity enforcement, access scoping for sensitive location traces, and the audit trails that let a regulated platform reconstruct any historical geometry state. These concerns are not afterthoughts layered onto a finished warehouse; they are encoded as SQL, tested in CI, and versioned alongside the models that produce them. This page maps the full surface area — from the dependency graph that orders spatial work, through adapter-specific execution and validation, to the failure modes that bite teams in production — and links out to the focused guides that implement each control.

The result is a transformation layer where raw coordinates become trusted, query-optimized spatial products. Whether you ship choropleth feeds to a BI tool, proximity features to a model store, or vector tiles to a mapping front end, the architectural backbone is the same: a layered dependency graph, canonical projections, validated geometries, and observable runs. The sections below walk that backbone end to end.

Layered spatial pipeline with per-layer materialization and governance gates A left-to-right data flow: raw geospatial sources feed a staging layer (view) that validates geometry and normalizes CRS, an intermediate layer (ephemeral or table) that runs spatial joins and dissolves, a marts layer (incremental, GiST-indexed) that simplifies and indexes for serving, and finally consumers such as BI, routing, and ML features. Below each layer a governance gate is attached: source freshness, validity plus CRS test, topology assertion, access grant, and audit log. Raw coordinates become trusted, query-optimized spatial products — one gate per layer Raw sources source · ephemeral GeoJSON · WKB Shapefile no mutation preserve fidelity Staging view ST_MakeValid ST_SetSRID 4326 ST_Transform normalize · typecast Intermediate ephemeral · table spatial joins ST_Union · buffer ST_DWithin partition-aware Marts incremental + GiST ST_Simplify tile-ready geom post-hook index seek, not scan Consumers serving BI · choropleth routing · tiles ML features entitled views GOVERNANCE GATES Source freshness Validity + CRS test Topology assert Access grant Audit log

Foundational Architecture & the Spatial Dependency Graph

A production-grade spatial architecture follows a layered, dependency-driven model that mirrors modern analytics engineering while explicitly accommodating the computational weight of spatial operations. The stack progresses from raw ingestion through staging, intermediate transformation, and finally analytics-ready marts. Each layer enforces a contract: raw layers preserve source fidelity without mutation; staging layers normalize types and apply foundational geometry constructors; intermediate layers execute heavy spatial joins and aggregations; marts expose curated, query-optimized geometries for downstream consumption.

The spatial model dependency graph is the architectural backbone. In spatial contexts the DAG must explicitly model geometric prerequisites. A polygon aggregation model cannot safely execute until the underlying point-in-polygon join completes; a topology-cleaning routine must run before any distance metric is calculated. By structuring ref() dependencies to reflect these spatial prerequisites, teams eliminate race conditions, prevent partial geometry states from leaking into marts, and enable incremental materializations that recompute only affected partitions.

sql
-- models/marts/mart_zone_metrics.sql
-- The mart depends on a cleaned-topology intermediate, which itself
-- depends on a validated staging model. The DAG enforces ordering.
{{ config(materialized='incremental', unique_key='zone_id') }}

select
    z.zone_id,
    z.geom,
    count(p.event_id) as event_count,
    st_area(z.geom::geography) as area_m2
from {{ ref('int_zones_cleaned') }} z
left join {{ ref('stg_events_validated') }} p
    on st_contains(z.geom, p.geom)
{% if is_incremental() %}
where z.updated_at > (select max(updated_at) from {{ this }})
{% endif %}
group by 1, 2

When designing for scale, account for the quadratic cost of unindexed spatial predicates. Strategic partitioning, bounding-box pre-filtering, and incremental state tracking are essential for handling large geospatial datasets without degrading pipeline latency or exhausting warehouse credits. The DAG is also where governance gates attach: a validity test on the staging node blocks promotion to intermediate, and a freshness check on the source blocks the whole subtree from running against stale feeds.

Core Concepts & Boundaries

Three concepts define the boundaries of every spatial model: the coordinate reference system it carries, the geometry type it uses, and the index that makes it queryable.

CRS governance. A coordinate reference system pins coordinates to the earth. Mixing EPSG codes is the most common silent failure in spatial pipelines: a join between EPSG:4326 (degrees) and EPSG:3857 (meters) compiles cleanly and returns geometrically meaningless results. Centralized spatial reference system management declares one canonical storage projection — typically EPSG:4326 for global storage, EPSG:3857 for web rendering — and forces every staging model to normalize to it. Apply ST_Transform at the staging boundary, never mid-join, so downstream models can assume a single SRID.

Geometry vs geography. GEOMETRY operates on a planar Cartesian model; GEOGRAPHY operates on a spheroid. Planar math is fast and exact for projected local data; spheroidal math is correct for global distances and areas but slower and supports fewer functions. A common pattern is to store GEOMETRY in EPSG:4326 and cast to ::geography only for the specific area or distance calculation that needs true-earth measurement, as the mart model above does with ST_Area.

SRID enforcement. A geometry with an undefined SRID (0) is a latent bug: indexes still build, but transforms and cross-CRS joins fail or misalign. Staging models should assert the SRID explicitly with ST_SetSRID(ST_GeomFromText(...), 4326) and reject any row whose SRID is unexpected.

Spatial index selection. The index determines whether a predicate is a scan or a seek. A GiST index is the general-purpose default for ST_Intersects, ST_Contains, and ST_DWithin over polygons and lines. SP-GiST favors point-heavy, non-overlapping data such as event streams. HNSW (and its IVFFlat cousin) target approximate nearest-neighbor search over vector/embedding columns rather than classic geometry, and matter when spatial features feed a similarity workload. Index choice is engine-specific and is detailed under index hints for spatial queries.

Adapter & Engine Comparison

Geospatial execution is highly adapter-dependent. PostGIS, Snowflake GEOGRAPHY, BigQuery GIS, and the DuckDB spatial extension each implement distinct type systems, indexing strategies, and function signatures. A robust architecture abstracts these differences through adapter-aware macros while respecting each engine’s lifecycle. The table below summarizes the trade-offs that drive choosing the right spatial adapter.

Capability PostGIS DuckDB spatial BigQuery GIS
Type model GEOMETRY + GEOGRAPHY, full SRID GEOMETRY (planar), GDAL-backed GEOGRAPHY only (WGS84 spheroid)
Function breadth Widest (ST_*, topology, raster) Broad and growing Curated subset, no planar GEOMETRY
Indexing GiST / SP-GiST, manual post-hook In-memory, partition pruning Transparent, S2-cell based
SRID handling Explicit, multi-SRID Explicit cast required Implicit EPSG:4326
CI suitability Heavy (container/service) Excellent (embedded, fast) Network-bound, costs credits
Best fit System of record, rich topology Local dev + CI validation Petabyte-scale serverless analytics

In PostGIS, GiST indexes are created post-materialization, requiring an explicit post-hook so the planner uses bounding-box filters. Snowflake and BigQuery manage indexing transparently but enforce strict SRID normalization at ingestion, so staging must validate the coordinate reference system before any cross-layer join. The DuckDB spatial extension leverages in-memory partitioning but requires explicit casting to GEOMETRY to trigger vectorized paths — which is precisely what makes it an ideal lightweight validator before promoting to a PostGIS system of record.

Pipeline Layer Responsibilities

Each layer owns a narrow, testable responsibility. Confusing those responsibilities is what produces unmaintainable spatial DAGs.

Staging validation checklist. Cast WKT/WKB to typed geometry, set and assert the SRID, repair invalid input with ST_MakeValid, drop or quarantine null geometries, and normalize the projection. Staging never aggregates and never joins across sources.

sql
-- models/staging/stg_parcels.sql
{{ config(materialized='view') }}

select
    parcel_id,
    st_setsrid(st_makevalid(geom), 4326) as geom,
    updated_at
from {{ source('cadastre', 'parcels_raw') }}
where geom is not null
  and st_isvalid(geom)

Intermediate transformation patterns. This is where spatial joins, ST_Union dissolves, buffering, and proximity work happen. Intermediate models assume staging guarantees (one SRID, valid geometry) and focus on correctness and partition-aware execution. Reusable logic here belongs in macros, covered next and in depth under optimizing proximity joins.

Mart materialization strategies. Marts are table or incremental, indexed, and simplified for the consumer. Use ST_Simplify (or ST_SimplifyPreserveTopology) to shrink vertices for tiling, materialize incrementally keyed on a stable surrogate, and attach a GiST index via post-hook so the serving query is a seek. Heavy projection rewrites belong in a geometry transformation pipeline rather than recomputed per query.

Macro & UDF Abstraction

Wrap spatial logic in a dbt macro the moment a pattern appears in two models or differs across adapters. Macros are where cross-engine portability and CRS policy become enforceable rather than aspirational. The canonical example is a CRS-normalization macro that compiles to the right function on each warehouse — the foundation of building custom spatial macros.

sql
-- macros/to_canonical_crs.sql
{% macro to_canonical_crs(geom_col, target_srid=4326) %}
    {%- if target.type == 'postgres' -%}
        st_transform({{ geom_col }}, {{ target_srid }})
    {%- elif target.type == 'duckdb' -%}
        st_transform({{ geom_col }}, 'EPSG:' ~ st_srid({{ geom_col }}), 'EPSG:{{ target_srid }}')
    {%- else -%}
        {{ geom_col }}  {# BigQuery GEOGRAPHY is implicitly WGS84 #}
    {%- endif -%}
{% endmacro %}

Parameterize the things that legitimately vary — target SRID, tolerance, distance threshold — and hard-code the things that must not, such as the canonical projection. Macros also keep post-hook index declarations DRY: a single spatial_index() macro can emit the correct CREATE INDEX ... USING gist statement and be referenced from every mart’s config.

Testing & Data Quality

Unlike scalar columns, spatial objects carry implicit topological rules: polygons must be closed, lines must not self-intersect, and multipolygons must keep consistent ring orientation. Embed validation directly in dbt tests so a bad geometry halts the DAG instead of reaching a dashboard. Use generic tests for the common guards and singular tests for topology assertions.

yaml
# models/staging/_staging.yml
version: 2
models:
  - name: stg_parcels
    columns:
      - name: geom
        tests:
          - not_null
          - dbt_utils.expression_is_true:
              expression: "st_isvalid(geom)"
          - dbt_utils.expression_is_true:
              expression: "st_srid(geom) = 4326"

Three guards cover most regressions: a not_null geometry guard, an ST_IsValid validity test, and an SRID assertion. Add ST_IsSimple where self-intersection matters, and a custom test for ring orientation where downstream tools are picky. Failing tests should be configured as error severity at staging so corrupted geometry never propagates to intermediate models, ML feature stores, or BI. Seed a handful of edge-case fixtures — a bowtie polygon, a zero-length line, a mixed-SRID row — so the test suite proves it actually catches what it claims to.

Performance & Scale Considerations

Spatial predicates are expensive because, without an index, every candidate pair is compared. Three levers control cost. First, ensure the planner uses a bounding-box index: in PostGIS that means a GiST index built in a post-hook and an ST_DWithin/&&-style predicate the planner can push down. Second, pre-filter with a cheap bounding-box (&&) before the exact predicate so expensive functions run on a small candidate set. Third, partition by a deterministic spatial grid (H3, S2, or a quadkey) so the warehouse prunes irrelevant partitions before any geometry is evaluated.

sql
-- mart with a GiST index applied after materialization
{{ config(
    materialized='table',
    post_hook="create index if not exists {{ this.name }}_geom_gix on {{ this }} using gist (geom)"
) }}

select zone_id, st_simplifypreservetopology(geom, 0.0001) as geom
from {{ ref('int_zones_cleaned') }}

Incremental materialization is the highest-leverage scale lever, but it trades freshness logic for compute savings: a late-arriving source row that shifts a polygon boundary must invalidate every downstream cell it touches. Track an updated_at watermark and, where boundaries move, recompute by affected grid cell rather than by row. These trade-offs are explored further in the guidance on handling large geospatial datasets.

CI/CD Integration

Spatial governance only holds if it runs on every pull request. The practical pattern is a two-tier CI: validate fast against an embedded engine, then promote to the production adapter. Run the model build and dbt test against the DuckDB spatial extension in GitHub Actions — it is embedded, starts in seconds, and exercises the same ST_* SQL — before promoting to a PostGIS service container for adapter-specific checks.

yaml
# .github/workflows/spatial-ci.yml
name: spatial-ci
on: [pull_request]
jobs:
  validate:
    runs-on: ubuntu-latest
    env:
      DBT_PROFILES_DIR: ./ci
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.11" }
      - run: pip install dbt-duckdb
      - run: dbt build --target ci_duckdb   # seeds geometry fixtures, runs models + tests

Seed deterministic geometry fixtures (small WKT seeds checked into the repo) so tests assert against known-good outputs rather than live data. Reference environment-specific connection details through dbt’s env_var() pattern — for example dbname: "{{ env_var('PGDATABASE') }}" in profiles.yml — so the same project runs unchanged across CI, staging, and production. Promotion to PostGIS should re-run the validity and SRID sweeps because adapter behavior diverges on edge cases the embedded engine tolerates.

Governance, Versioning & Access Scoping

Schema evolution in spatial pipelines introduces challenges scalar tables never face. Adding a geometry column, changing an index strategy, or migrating from GEOGRAPHY to GEOMETRY is a breaking change requiring coordinated version control. A disciplined approach to versioning spatial schemas in dbt lets teams ship backward-compatible transformations, run parallel model branches during migration windows, and roll back topology-breaking changes without disrupting dependent analytics.

Access control for location data demands a different paradigm than traditional PII. Coordinate traces, geofence memberships, and spatial aggregations can inadvertently expose movement patterns or proprietary infrastructure. Enforcing data security and scoping rules at the warehouse level — combined with dbt access grants and row-level security — ensures each consumer group, from executive dashboards to field operations, sees only the geometry it is entitled to.

Reproducibility and auditability close the loop. Every spatial transformation must be traceable to its source inputs, transformation logic, and execution environment. Capture model run metadata, lineage graphs, and spatial validation results in structured log tables alongside each dbt invocation. Comprehensive audit trails let platform teams reconstruct historical geometry states, validate compliance with geospatial standards, and diagnose coordinate drift across incremental runs — turning governance from a reactive compliance exercise into a proactive engineering discipline.

Common Failure Modes & Remediation

Symptom Root cause Remediation
Joins return empty or misaligned results Mismatched SRIDs across sources Normalize to canonical CRS at staging with ST_Transform; assert ST_SRID = 4326 in tests
ST_Transform returns NULL or errors Input geometry has SRID 0 / undefined Set SRID explicitly via ST_SetSRID before transforming
Topology functions fail mid-run Invalid geometry (self-intersection, unclosed ring) Repair with ST_MakeValid at staging; gate with an ST_IsValid test
Query times out on large tables Missing or unused spatial index Add GiST index in post-hook; pre-filter with bounding-box &&
Index bloat after many incremental runs Dead tuples accumulating around the GiST index Schedule REINDEX/VACUUM via a maintenance hook
CI passes but PostGIS run fails Adapter version / function-signature mismatch Pin adapter versions; re-run validity sweeps on the production target before promotion

Treating each of these as a named, testable condition — rather than a one-off incident — is what keeps a spatial architecture trustworthy as it grows. Most reduce to the same root disciplines this page has traced: canonical CRS, validated geometry, the right index, and observable runs.

↑ Back to dbt + Geospatial: Transforming Spatial Data in the Modern Stack

Explore this section