Geospatial Orchestration Architecture & Fundamentals

A geospatial pipeline is an ordinary data pipeline right up to the point where it isn’t, and every difference comes from the same source: the data has an extent, a projection and a wildly uneven distribution over the ground. Those three properties decide the shape of the graph, the size of the workers, where the money goes, what a credential must reach and how a deployment can fail. This section is about the decisions that follow from them — the ones taken early, encoded everywhere, and expensive to revisit.

There are seven of them. What is the unit of work. Where does state live. Which orchestrator models that unit best. What does a run cost and why. Who can reach the data, and how precisely. How does the native stack ship. And what does the pipeline divide the world into. None is difficult in isolation; together they determine whether a pipeline that works in June is still operable in December.

The order in which they bind matters more than the order in which they are implemented. The unit of work constrains everything else — a ledger cannot be keyed on something that has no canonical name, a partition cannot be smaller than the thing being retried, a credential cannot be scoped to a prefix that does not exist. So the sequence below is a dependency order rather than a schedule; in practice all seven are built together, and the value of naming them separately is knowing which one to revisit when something further along turns out to be awkward.

Core Architectural Layers

The unit layer. A tile index, a scene identifier, a municipality code — the thing a run is made of. It fixes the retry granularity, the cache key, the failure blast radius and the fan-out width simultaneously, which is why DAG design principles for spatial ETL treats it as the first decision rather than an implementation detail.

The state layer. What has been built, how far a long job got, and how current the output is. Three different questions with three different homes, covered in state management in geospatial flows.

The orchestration layer. Whether the tool models runs or the things runs produce, which decides how much bookkeeping you write yourself. Prefect vs Dagster for GIS workloads sets out the trade.

The resource layer. Worker memory, upstream concurrency, bytes read, instance market. Cost optimization for spatial compute shows where the money actually goes, which is rarely where people look first.

The trust layer. Credentials scoped to a job, sources treated as untrusted input, coordinates treated as personal data. Security boundaries for spatial data covers all three.

The delivery layer. One immutable image, a boot-time assertion, a canary that reprojects. Deployment & CI/CD for spatial workers explains why the discipline is stricter here than for a service.

The partition layer. Grids, administrative units or hierarchical cells, and what to do about the skew all three have. Partitioning strategies for spatial workloads profiles them against real data.

Seven decisions, in the order they bindThe unit of work is decided first and constrains everything after it: state, orchestrator, resources, trust boundaries, partitioning and delivery all follow from what a run is made of.unit of workdecided firststateledger, checkpointorchestratorruns or assetspartitioninggrain and skewcostsecuritydeliveryChanging anything on the left invalidates work on the right, which is why the leftmost box deserves an afternoon.
The arrows are dependencies rather than a sequence: the layers are built together, and the left-hand decisions constrain the right-hand ones.

None of these layers is optional, and several are frequently implicit. A pipeline with no explicit state layer still has one — it is the orchestrator’s task history, which happens to be the wrong granularity. A pipeline with no partition decision still has a partition, which is whatever the first fan-out happened to iterate over. The difference between an implicit and an explicit choice is not usually correctness on day one; it is whether the choice can be found and changed by somebody who was not there when it was made.

Key Design Constraints Imposed by Geospatial Data

Extent means the graph’s width is data-dependent. A footprint decides how many tiles a run produces, so a wrong zoom is a hundred thousand scheduled tasks rather than a raised exception. Every fan-out needs a guard and a manifest written before it happens.

Projection means correctness is invisible. A wrong CRS produces valid files, complete coverage and a layer in the wrong place. Only an explicit comparison against a known answer catches it, which is why validating coordinate systems before ETL exists and why a boot-time transformation probe is worth having on every worker.

Density means uniform partitions carry non-uniform work. The largest cell in a national grid is routinely hundreds of times the median, so a run’s duration is set by one city while the fleet idles.

Size means memory is the binding constraint. Spatial work is memory-hungry in short spikes, so per-task peak times concurrency is the number that sizes a fleet — and the number nobody has measured.

Sources are other people’s. Publishers change projections, truncate deliveries, republish unchanged files nightly and occasionally stop. A pipeline over public geodata spends much of its design budget on that fact.

The native stack decides the answers. GDAL, PROJ and their grids determine what a reprojection returns, so the deployment artefact is closer to a specification of correctness than to packaging.

Topic Deep-Dives

DAG design principles for spatial ETL

Shape the graph from the data’s natural partitions rather than from the verbs a script performs. The topic covers granularity, real dependency edges and the two limits every fan-out needs; its guides cover structuring a raster DAG, parametrizing by tile index, mapping over a grid in Dagster and bounding the fan-out.

State management in geospatial flows

The ledger records what exists, the checkpoint records how far a job got, the watermark records how current the source is. The topic keeps them apart; its guides cover checkpointing mosaics, incremental vector deliveries, choosing a home for each and resuming from a checkpoint.

Prefect vs Dagster for GIS workloads

One models runs, the other models the things runs produce, and the difference decides how you answer “which tiles are missing”. The topic compares them properly; its guides cover what Prefect’s states mean, migrating between them, where Airflow fits and what a task run costs.

Cost optimization for spatial compute

Three levers — measured memory, bytes read, and work skipped — dwarf the instance-price question everyone starts with. The topic covers all three; its guides cover sizing workers, interruptible capacity and range reads.

Security boundaries for spatial data

A coordinate can identify a person, a source file is input to a large C parser, and a fan-out multiplies every credential decision by its width. The topic covers the three boundaries; its guides cover database connections, logs and object-storage credentials.

Deployment & CI/CD for spatial workers

The native stack decides the numbers, so the deployment artefact has to be pinned, asserted and compared rather than merely started. The topic covers the chain; its guides cover the image, the lock file, parity, fixtures and upgrades.

Partitioning strategies for spatial workloads

Grids are simple and skewed, administrative units match the data and vary hugely, hierarchical cells sit between. The topic profiles all three on real data; its guides cover raster grain, H3 cells and the skew every scheme has.

Where to start if you are starting now

Decide the unit of work, give it a canonical name, and derive a content-based key from it. Almost everything else in this section becomes easier once that exists: the ledger has a primary key, the cache has something to hash, the partition has a name, the credential has a prefix to scope to, and the coverage question has a denominator. Pipelines that skip it spend the following year adding each of those separately.

One key, five things it enablesA work key derived from the unit, the source digest and the recipe version becomes the ledger's key, the cache's key, the partition name, the credential's prefix and the coverage denominator.work keyunit + digest + recipeledger primary keycache keypartition namecredential prefixcoverage denominatorEach of these is a projectto add separately, and aline of code to derivefrom a key that exists.
The key is the cheapest thing in this section to get right and the most expensive to add once five subsystems have invented their own.

How the layers show up in one run

It is worth walking a single nightly run through all seven, because they are much easier to hold together as a narrative than as a list. The run begins by asking the source what has changed, which is the watermark and the digest — the state layer. It computes a manifest of affected units from a footprint, which is the unit and partition layers deciding the graph’s width. It acquires a session scoped to one source prefix and one output prefix, which is the trust layer. It fans out under two concurrency limits sized from measured memory and a publisher’s tolerance, which is the resource layer. Each task consults the ledger, does nothing if the key matches, and writes a checkpoint if the work is long. The reduction refuses to assemble below a coverage floor, and the publish step stamps the image digest onto every object, which is the delivery layer making the run auditable afterwards.

Nothing in that narrative is exotic, and every step of it is a place where a pipeline that skipped the corresponding decision has to improvise. The improvisations are individually reasonable and collectively expensive: a manifest computed inline so the width is never logged, a credential shared because scoping it was fiddly, a ledger that is really a cache, a coverage check that was going to be added later. Each is a small shortcut whose cost arrives during an incident, when the question is which tiles are wrong and the pipeline cannot say.

Implementation Patterns & Code Scaffold

The scaffold below is the smallest thing that has all seven decisions in it. Everything else in this section is an expansion of one of these lines.

from __future__ import annotations

import hashlib
from dataclasses import dataclass
from pathlib import Path

from prefect import flow, task, get_run_logger
from prefect.concurrency.sync import concurrency


@dataclass(frozen=True, slots=True)
class Tile:
    """1. The unit of work, canonical and validated at construction."""

    z: int
    x: int
    y: int

    def __post_init__(self) -> None:
        if not (0 <= self.z <= 22 and 0 <= self.x < 2 ** self.z and 0 <= self.y < 2 ** self.z):
            raise ValueError(f"non-canonical tile index z{self.z}/{self.x}/{self.y}")

    @property
    def path(self) -> str:
        return f"{self.z}/{self.x}/{self.y}"


def work_key(tile: Tile, source_digest: str, recipe: str) -> str:
    """2. Content-derived, so an unchanged source does no work."""
    return hashlib.sha256(f"{tile.path}|{source_digest}|{recipe}".encode()).hexdigest()[:16]


@task(timeout_seconds=300)
def plan(footprint_wkt: str, zoom: int) -> list[Tile]:
    """3. The graph's width, decided and guarded before anything fans out."""
    tiles = [Tile(zoom, x, y) for x, y in tiles_intersecting(footprint_wkt, zoom)]
    if not tiles:
        raise ValueError("empty manifest — footprint and zoom disagree")
    if len(tiles) > 20_000:
        raise ValueError(f"{len(tiles)} tiles at z{zoom} is implausible")
    get_run_logger().info("manifest: %d tiles at z%d", len(tiles), zoom)
    return tiles


@task(retries=2, retry_delay_seconds=[15, 60], timeout_seconds=900, tags=["tile"])
def build(tile: Tile, source: SourceSpec, recipe: str, session) -> str | None:
    """4. Bounded twice, idempotent, and recorded only once the write is durable."""
    key = work_key(tile, source.digest, recipe)
    if ledger_has(key):
        return tile.path
    with concurrency(f"source:{source.host}", occupy=1, timeout_seconds=1800):
        window = fetch_window(source, tile, session)     # 5. scoped session
    write_tile(warp(window, tile), tile, session)
    ledger_mark(key, tile)                                # 6. after the object exists
    return tile.path


@flow(name="tile-build", timeout_seconds=7200)
def tile_build(footprint_wkt: str, zoom: int, source: SourceSpec,
               recipe: str = "v4", coverage_floor: float = 0.995):
    tiles = plan(footprint_wkt, zoom)
    with job_session(source.prefix, output_prefix=f"tiles/{recipe}") as session:
        states = build.map(tiles, source=source, recipe=recipe,
                           session=session, return_state=True)

    done = [s for s in states if s.is_completed()]
    coverage = len(done) / len(tiles)
    get_run_logger().info("tiles: %d/%d, coverage %.4f", len(done), len(tiles), coverage)
    if coverage < coverage_floor:
        # 7. The run's verdict reflects the product, not merely the execution.
        raise RuntimeError(f"coverage {coverage:.4f} < {coverage_floor} — not publishing")
    return publish(done, recipe=recipe)

Four properties of that scaffold are worth naming, because they are what the rest of this section defends. The manifest is written before the fan-out, so the width is inspectable rather than discovered. The work key is content-derived, so a re-run over an unchanged source is nearly free. The ledger is written after the object exists, so a crash leaves an orphaned object rather than a phantom record. And the coverage floor makes the run’s final state a statement about the product rather than about whether the code returned.

A fifth property is easy to miss and worth pointing at: the domain functions the tasks call — tiles_intersecting, fetch_window, warp, write_tile — contain no orchestrator concepts at all. That separation is what makes the pipeline testable without a control plane, portable between orchestrators, and reviewable by somebody who knows geospatial processing but not this particular framework. It costs nothing to maintain if it is established at the start and is genuinely difficult to recover once decorators have accumulated business logic.

Failure Modes & Operational Guardrails

The graph is wider than anyone expected. A footprint that grew, a zoom typo, a source whose extent became global. The guard in plan costs two lines and converts four hundred thousand scheduled tasks into a raised exception.

A retry storm reaches somebody else’s server. Task retries multiply by flow retries, and a fan-out multiplies both. Retry at one level, bound the concurrency at the resource, and see limiting DAG fan-out.

Everything rebuilds every night. A timestamp entered the work key, or the source republishes unchanged files. The skip rate is the metric, and a sudden drop to zero deserves an alert.

A worker runs out of memory intermittently. Concurrency times per-task peak exceeded the limit, usually because the driver cache was left at its default. Right-sizing workers has the arithmetic.

The layer is in the wrong place and nothing failed. A missing transformation grid, a flipped tile axis, a CRS assumed rather than checked. Only a comparison against a known answer catches any of them.

One partition sets the duration of the run. Adding workers does not help; the critical path is one unit. Weigh before scheduling, as in handling skewed partitions.

A dependency upgrade changed the outputs. Nothing errored. Diff a shadow run’s tiles against the previous digest’s before promoting; see rolling out GDAL upgrades.

Which failures announce themselvesThree of the seven common failure modes raise errors. Four are silent: they produce complete, valid, wrong output, and only an explicit comparison detects them.failureannounces itself?detectorgraph too wideyesmanifest guardout of memoryyesthe kernelretry stormeventuallytheir rate limitrebuilds everythingnoskip-rate metricwrong place on Earthnoa known answerThe bottom two rows are the ones that reach consumers, and neither has a detector unless somebody built one.
Most of the unusual practice in this section exists for the red rows; the green ones are handled by ordinary engineering.

Reading the guardrails as a checklist

Each of those failure modes maps to a control that costs very little to add and a great deal to add late. A manifest guard is two lines in the planning task. A retry budget is a decision about which level retries rather than a mechanism. A skip-rate log line is one call. A memory histogram is three. A transformation probe is a known coordinate and an assertion. A weight query is one grouped SELECT. A shadow diff is a flow that already exists, run twice. The whole list is perhaps a day of work spread over the life of a pipeline, and it is the difference between an incident that is diagnosed in ten minutes and one that is diagnosed by reading code.

The reason to treat them as a checklist rather than as good practice is that none of them is prompted by anything. A pipeline without a manifest guard behaves perfectly until the day it does not; a pipeline without a skip-rate metric never complains about rebuilding everything. These are all controls whose absence is silent, so they have to be added deliberately, and the natural moment to do that is when the architecture is being decided rather than after the first surprise.

Toolchain & Dependency Matrix

Layer Typical choice Why, spatially
Orchestrator Prefect or Dagster Runtime-shaped fan-outs favour the first; fixed grids with backfills favour the second.
Ledger and watermark PostGIS The unit has a geometry, so coverage questions are spatial queries.
Checkpoints and outputs object storage Large, opaque, read by key, disposable under a lifecycle rule.
Raster stack GDAL, rasterio Pinned exactly; it decides transformation and resampling results.
Vector stack fiona, shapely, pyproj Must link the same GDAL and GEOS as the raster half, or the two disagree.
Format cloud-optimised GeoTIFF, GeoPackage, FlatGeobuf Window reads are the difference between two megabytes and twelve gigabytes.
Partition key tile index, admin code or H3 cell Canonical, stable, and derivable into a work key.
Packaging conda-lock plus a pinned base image The native stack is the artefact; the Python code is the smallest layer.
Observability Prometheus, OpenTelemetry, structured logs Covered in observability & monitoring.

Frequently Asked Questions

Which decision should be made first?

The unit of work, because every other decision on this page takes it as an input. It determines the ledger’s primary key, the cache key, the partition name, the credential’s prefix and the coverage denominator, and each of those is cheap to derive from a key that exists and a project to add separately.

Can these decisions be deferred?

Some, at a known price. The orchestrator can be changed later if the domain code was kept free of it. The partition scheme is much harder, because it is encoded in cache keys and coverage history. The unit of work is hardest of all — it is the thing everything else is named after.

How much of this applies to a small pipeline?

The unit, the key and the coverage check apply at any size and cost an afternoon. Scoped credentials, checkpointing and a canary earn their place once a failure has a consequence beyond the team. Nothing here needs a large pipeline to be worth doing; several of these things only become expensive when added late.

Do these decisions differ for raster and vector work?

The layers are identical and the specifics are not. Raster work is dominated by memory and bytes read, so the resource layer does most of the work; vector work is dominated by feature counts, geometry validity and transactional writes, so the state layer does. The partition families differ too — grids and processing tiles on one side, administrative units and hierarchical cells on the other. A pipeline doing both should expect two sets of numbers under one set of decisions.

How does this change for near-real-time work?

The decisions stay; the schedule stops being the trigger. An event-driven pipeline learns which units are affected from each delivery rather than enumerating them from a footprint, which makes the partition set dynamic and the watermark per-stream rather than per-run. Everything about keys, ledgers, credentials and coverage carries over unchanged, which is the main practical argument for making them explicit before the pipeline needs to be reactive.

What is the single most common mistake?

Treating the pipeline as a data pipeline that happens to carry coordinates. Every unusual practice in this section — the manifest guard, the transformation probe, the coverage floor, the shadow diff — exists because a spatial pipeline’s characteristic failure is complete, valid, plausible output in the wrong place, and ordinary data engineering has no detector for that.

Is there a smallest sensible version of all this?

Yes, and it is about a day. Give the unit a validated type with a canonical string. Derive a work key from it, the source digest and a recipe version. Write the manifest before the fan-out and log its width. Consult the key before doing work and record it after the output is durable. Check coverage before publishing. That is five things, they fit in a single file, and they are the substrate every other page in this section builds on — a pipeline with them can adopt anything else here incrementally, and one without them will find each addition harder than it should be.

Where does observability fit?

Alongside all seven layers rather than after them; the counters, traces and logs are what turn each of the guardrails above into something you can see. Observability & monitoring for geospatial pipelines covers the signals, and the per-unit histograms it describes are what the cost and skew work here depends on.

And resilience?

The same relationship. Retries, breakers, dead letters, idempotency and timeouts are how a pipeline survives sources it does not control; resilience & failure handling for GIS pipelines treats them, and this section’s decisions determine what a retry means and how big a failure’s blast radius is.

How does this section relate to task design?

This section decides the shape; spatial task design & dependency mapping decides what happens inside a task — chaining, caching, branching, validation and the fan-out itself. The boundary is roughly between decisions that are expensive to change and decisions that are not.

Geospatial Orchestration