Data Contracts for Spatial Products

A data contract for a spatial product is the machine-checkable agreement that pins down five things at once — schema, coordinate reference system, geometry precision, service-level guarantees, and semantics — and enforces them at the exact boundary where a producing domain hands data to a consuming one. This reference sits inside Geospatial Data Mesh Fundamentals and treats the contract as executable infrastructure, not a wiki page: if a producer changes its CRS from EPSG:4326 to EPSG:3857, drops a required attribute, or coarsens geometry precision below the promised tolerance, the boundary rejects the payload before any consumer resolves it. Platform engineers and GIS data stewards should read this alongside Schema Contracts for Vector Tile Data, which applies the same discipline to the tile-serving layer, and Product Thinking for GIS Datasets, which supplies the ownership and SLA model that a contract encodes. Where the two diverge, the version stamp v1.2.0-crs:EPSG:4326-res:10m is the arbiter — it is the immutable identity a consumer pins to.

Figure — A spatial data contract mediates the producer/consumer boundary, checking schema, CRS, geometry precision, SLA, and semantics before stamping an immutable version on the payload.

Producer to contract gate to consumer flow with contract fields and version stamp A producing domain team sends a spatial product left to right into a central data-contract gate positioned at the producer/consumer boundary. The gate checks five fields listed inside it: schema, CRS plus axis order, geometry precision, SLA, and semantics. A payload that passes flows on to the consumer, which machine-checks the contract on receipt; a payload that violates any field is blocked. The gate emits an immutable version stamp reading v1.2.0-crs:EPSG:4326-res:10m that the consumer pins to. pass Producer domain team Data contract producer/consumer boundary schema CRS + axis order geometry precision SLA semantics Consumer machine-checked v1.2.0-crs:EPSG:4326-res:10m immutable version stamp

Architectural Boundaries & Design Rationale

The contract exists because a federated mesh removes the single geodatabase that once forced every producer and consumer to share one implicit schema. Once each domain owns and publishes independently, “the data changed and nothing told me” becomes the dominant failure mode. A data contract restores the missing signal by making the boundary itself refuse any product that no longer matches what consumers agreed to depend on. It is enforced at ingress — the same zero-trust posture the catalog applies in Metadata Cataloging for Raster/Vector — so a non-conforming payload never reaches storage, discovery, or a downstream join.

Five surfaces make up the contract, and each maps to a distinct class of silent breakage:

  • Schema — attribute names, types, nullability, and cardinality. Removing a required field or narrowing a type breaks every consumer that projected it.
  • CRS — the declared crs_epsg and axis order. A producer that flips from EPSG:4326 to EPSG:32633, or serves lat/lon where consumers expect lon/lat, corrupts every spatial predicate downstream. The governance model for this surface lives in CRS Governance and Reprojection Standards.
  • Geometry precision — coordinate resolution and the topology rule set. A product promising sub-metre planar topology that ships decimated geometry fails validation rather than quietly degrading a routing graph.
  • SLA — freshness, availability, and latency targets. These are the guarantees a consumer plans capacity against, drawn from the same model as Product Thinking for GIS Datasets.
  • Semantics — units, enumerations, null conventions, and the meaning of each field. A population column that switches from count to density is schema-valid and semantically catastrophic.

The rationale for enforcing all five at one gate rather than scattering checks across pipelines is idempotency and single-point accountability: a payload validated once against a pinned contract version behaves identically on every retry, and a breach has exactly one place to page. Contract evolution is governed by Versioning Spatial Data Contracts with SemVer, which decides whether a change is a compatible MINOR bump or a breaking MAJOR that forces a parallel version.

Specification & Contract Reference

A spatial data contract is a versioned document, stored beside the product and referenced by its stamp. The mandatory fields below are the minimal surface every producer must publish and every consumer may rely on.

Field Type Required Constraint / Convention
contract_version string yes v1.2.0-crs:EPSG:4326-res:10m site convention; immutable once published
product_id UUID yes Stable identity across contract versions
crs_epsg integer yes EPSG code; change is always a MAJOR bump
axis_order enum yes lon_lat | lat_lon; explicit to avoid silent inversion
geometry_type enum yes point | linestring | polygon | multipolygon | raster
coordinate_precision integer yes Decimal places retained; drives precision validation
topology_rule_set enum vector only none | planar | network | 3d_solid
schema_fields array yes Ordered {name, type, nullable, unit} records
freshness_sla_minutes integer yes 0 means real-time; drives drift alerts
availability_target number yes Fractional, e.g. 0.999; consumer capacity input
semantics object yes Per-field units, enums, and null meaning
product_owner_id email yes Accountable steward for breaches and incidents

How a contract change reaches consumers without a coordinated releaseA proposed contract change moves through four states. It is drafted from a structural diff against the current version, classified as major, minor or patch by a consumer-driven compatibility rule, published immutably alongside the current version, and finally becomes current once consumer-driven contract tests pass. Two return paths exist: a classification the diff cannot justify sends the proposal back to draft, and a failing consumer test sends a published candidate back rather than promoting it — so a breaking change is caught by a consumer test rather than by a consumer.Draftedstructural diffClassifiedMAJOR / MINOR / PATCHPublishedimmutable, parallelCurrentconsumers migrateddiff computedstampedconsumer tests passclassification unsupported by the diffconsumer contract test fails — caught by a test, not a consumer

The enforcement surface — where the contract is evaluated — is carried in transport and policy inputs rather than the payload body, so the same contract can gate a REST ingress, a stream consumer, or a CI check.

Surface Key Purpose
HTTP header X-Contract-Version Pins the exact contract the payload claims to satisfy
Policy input input.contract.crs_epsg Validated against the CRS allowlist and the pinned version
Policy input input.payload.schema_hash Detects drift between declared and actual schema
Idempotency key SHA-256(product_id ‖ contract_version ‖ payload_hash) Makes re-validation of the same payload side-effect free
Registry entry contract://<product_id>/<contract_version> Canonical, immutable storage path for the contract document

Consumers subscribe to a specific contract_version and are notified only when a compatible MINOR/PATCH supersedes it; a MAJOR requires an explicit, tested migration rather than an automatic upgrade.

Production Implementation

The contract is authored as a machine-readable document and enforced by a policy engine at the boundary. Below is a data-contract schema in YAML (the human-authored source) and its JSON Schema projection used for structural validation, followed by an opa eval gate that checks the cross-cutting rules JSON Schema cannot express — CRS allowlisting, version pinning, and ownership. The gate is idempotent and zero-trust by default: identity is verified upstream, default allow = false denies anything not explicitly permitted, and re-evaluating the same payload against the same contract version always yields the same decision.

yaml
# contract://parcels_cadastre/v1.2.0  — authored source of truth
contract_version: "v1.2.0-crs:EPSG:4326-res:10m"
product_id: "7c9e6a1b-2f34-4d8a-9b11-0a5c2e7f4d21"
crs_epsg: 4326
axis_order: lon_lat
geometry_type: multipolygon
coordinate_precision: 6          # ~0.11 m at the equator
topology_rule_set: planar
schema_fields:
  - { name: parcel_id,   type: string,  nullable: false, unit: none }
  - { name: area_m2,     type: number,  nullable: false, unit: square_metre }
  - { name: land_use,    type: string,  nullable: false, unit: enum }
freshness_sla_minutes: 1440
availability_target: 0.999
semantics:
  land_use: { enum: [residential, commercial, agricultural, industrial] }
  area_m2:  { null_means: not_applicable }
product_owner_id: cadastre-steward@example.org
json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "SpatialDataContract",
  "type": "object",
  "required": ["contract_version", "product_id", "crs_epsg", "axis_order",
               "geometry_type", "coordinate_precision", "schema_fields",
               "freshness_sla_minutes", "availability_target", "product_owner_id"],
  "properties": {
    "contract_version": { "type": "string", "pattern": "^v\\d+\\.\\d+\\.\\d+-crs:EPSG:\\d{4,5}-res:.+$" },
    "product_id": { "type": "string", "format": "uuid" },
    "crs_epsg": { "type": "integer", "minimum": 1000, "maximum": 99999 },
    "axis_order": { "type": "string", "enum": ["lon_lat", "lat_lon"] },
    "geometry_type": { "type": "string",
      "enum": ["point", "linestring", "polygon", "multipolygon", "raster"] },
    "coordinate_precision": { "type": "integer", "minimum": 0, "maximum": 15 },
    "topology_rule_set": { "type": "string", "enum": ["none", "planar", "network", "3d_solid"] },
    "schema_fields": { "type": "array", "minItems": 1 },
    "freshness_sla_minutes": { "type": "integer", "minimum": 0 },
    "availability_target": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 },
    "product_owner_id": { "type": "string", "format": "email" }
  },
  "additionalProperties": false
}
rego
package contract.boundary

import rego.v1

default allow = false

# Zero-trust: the payload is admitted only if it pins the published contract
# version, declares an allowlisted CRS, and its schema hash matches the contract.
allow if {
    input.header["x-contract-version"] == input.contract.contract_version
    input.contract.crs_epsg in [4326, 3857, 32610, 32611, 32612, 32633]
    input.payload.schema_hash == input.contract.schema_hash
    input.payload.coordinate_precision >= input.contract.coordinate_precision
}

Run the JSON Schema check and the policy together at ingress. In CI, opa eval -i request.json -d boundary.rego "data.contract.boundary.allow" fails the producer’s merge before a breaking payload is ever published; at runtime the same bundle runs as an admission sidecar so the boundary, the registry, and the consumer all enforce the identical contract independently. A false decision returns 409 Conflict with the offending field, and the producer must either fix the payload or, if the change is intentional, cut a new contract version.

Diagnostic Runbook

When a consumer reports broken or empty results downstream, isolate the failing contract surface in order from transport to semantics.

  1. Version pin mismatch (HTTP 409). Compare the X-Contract-Version header against the registry entry the consumer subscribed to. A mismatch means a producer published a new version without a compatible migration. Remediation: re-tag the payload with the pinned version, or coordinate a MAJOR migration per Versioning Spatial Data Contracts with SemVer.
  2. Schema-hash drift. If input.payload.schema_hash differs from input.contract.schema_hash, the producer changed columns without bumping the contract. Remediation: diff the field list, classify the change, and republish under the correct version bump.
  3. CRS or axis-order rejection. An allow = false with a matching version usually means crs_epsg fell outside the allowlist or axis_order was inverted. Remediation: reproject at the boundary per CRS Governance and Reprojection Standards; never widen the allowlist to force a pass.
  4. Precision violation. coordinate_precision below the contract minimum fails the policy. Remediation: stop decimating geometry upstream, or bump the contract if the coarser precision is a deliberate, communicated change.
  5. Semantic breakage that passes schema. Values are schema-valid but wrong (units flipped, enum extended). Remediation: add the changed enum/unit to the contract semantics block and treat an incompatible change as MAJOR.
  6. Idempotency collision. The same product_id and contract_version produced two divergent payload hashes. Remediation: return the existing record for identical payloads; reject divergent ones with 409 and require a version bump.
  7. SLA breach without a schema fault. Data is valid but stale or unavailable. Remediation: page the product_owner_id, trigger a producer re-run, and check the freshness monitor against freshness_sla_minutes.

SLA Targets & Performance Baselines

Contract enforcement is itself a product surface with guarantees. These baselines apply per boundary and feed the alerting that governs remediation.

Metric Target Alert Threshold Remediation Action
Contract validation latency (p95) < 30 ms > 90 ms for 10 min Cache compiled policy bundle; co-locate the gate sidecar
Breaking-change escape rate 0 any breach reaching a consumer Add the missing rule; block on CI, not runtime
Schema-drift detection lag < 1 publish cycle drift found post-publish Move the hash check into the producer’s merge gate
Contract-version pin coverage 100% of consumers any unpinned consumer Require X-Contract-Version; deny unpinned reads
Freshness vs freshness_sla_minutes within SLA 2× SLA exceeded Re-run producer pipeline; page owner
Idempotent re-validation 100% identical decisions any nondeterministic result Pin policy bundle version; remove clock/random inputs

What Belongs in the Contract, and What Must Not

A contract is only useful if both sides can rely on it, which means every clause has to be something the producer can genuinely commit to and the consumer can genuinely verify. Contracts fail in two symmetrical ways: they promise things the producer cannot control, and they omit things the consumer silently depends on.

The clauses that belong are those that are observable from the output port alone. A consumer can verify a geometry type, a CRS, an attribute’s presence and type, a coordinate precision, a bounding box, a resolution, and an update cadence, all without any knowledge of how the product is made. Each of these can be asserted mechanically at request time and each of them, if violated, breaks a consumer in a way that is attributable. These are the load-bearing clauses.

Clause Belongs in the contract because Verified by
Native CRS Determines what every coordinate means Reading the manifest; comparing against a known control point
Geometry type & dimensionality A consumer’s spatial predicates depend on it Schema validation at the boundary
Attribute schema & nullability Drives every downstream join and filter Contract test against a sample response
Coordinate precision Sets the resolution at which equality is meaningful Inspecting returned coordinates
Declared extent Distinguishes “no data here” from “outside coverage” Comparing a query bbox against the manifest
Resolution / scale Determines fitness for a consumer’s use Metadata; measured feature density
Update cadence The basis of every freshness expectation Observed publication timestamps
Availability & latency SLO What the consumer designs their own SLO against The producer’s published indicators

Which clauses belong in a contract, and which couple a consumer to an implementationSix candidate clauses judged on whether a consumer can verify them from the output port alone and whether committing to them constrains the producer unnecessarily. Native CRS, geometry type, attribute schema and declared extent are all observable from the port and belong in the contract. Storage format and index strategy are not observable and must not be committed to, because promising them turns an internal improvement into a breaking change. The final row names the category that causes most trouble: undeclared behaviour like feature ordering, which consumers depend on and no one wrote down.Consumer can verifyVerdictNative CRSyesin the contractAttribute schemayesin the contractDeclared extentyesin the contractStorage formatnokeep it internalIndex strategynokeep it internalFeature orderingobserveddeclare unspecified

The clauses that do not belong are internal mechanisms. Storage format, indexing strategy, partitioning scheme, tile pyramid internals, the database product, the orchestrator — none of these should appear in a contract, because committing to them removes the producer’s freedom to improve without a breaking change, and because a consumer who depends on them has coupled to something the boundary was supposed to hide. A contract that specifies “features are stored in PostGIS with a GiST index” has converted an implementation choice into a promise, and the first time the domain moves to a columnar store it will be a MAJOR version bump for no consumer-visible reason.

There is a third category worth naming explicitly, because it causes the most trouble: things consumers depend on that nobody wrote down. Feature ordering is the classic example — a consumer builds a pipeline that assumes results arrive in a stable order, the producer changes an index, and the consumer breaks with no contract having been violated. Others include the presence of a particular attribute value, the maximum number of features in a response, the behaviour at the antimeridian, and whether an empty result is a 200 with zero features or a 404. The remedy is not to enumerate every possible dependency in advance; it is to state the negative clauses explicitly. A contract that says “feature order is unspecified and will change” gives the consumer a fair warning and the producer a defensible position, and it costs one line.

Finally, a contract must state what happens when it cannot be met. A product that is unavailable, stale, or degraded is in a state the contract should describe — which response a consumer receives, how degradation is labelled, and how long the producer commits to holding the previous version live during a deprecation. A contract that only describes the happy path leaves the most consequential moments undefined, which is exactly when both sides most need to know what to expect.

One practical note on where the contract lives. A contract that exists only as a document describing an endpoint will drift from the endpoint within a release or two, because nothing forces them to agree. The arrangement that holds is for the machine-readable contract to be the artifact from which both the boundary validator and the published documentation are generated — so a clause that is not enforced cannot be published, and a clause that is enforced cannot be omitted from the documentation. Where that generation step exists, contract drift stops being a discipline problem and becomes a build failure.

Governance & Compliance Notes

Every contract version is immutable and append-only. Publishing v1.3.0 never mutates v1.2.0; the older document stays queryable for a defined retention window (typically 90 days) so audit and rollback can reconstruct exactly which contract a historical payload satisfied. Each boundary decision emits an audit event capturing the actor, the product_id, the resolved contract_version, the policy result, and the offending field on denial — the same evidentiary discipline the catalog applies at registration. The CRS allowlist in the Rego gate is the single enforcement point for approved projections, so an unapproved crs_epsg cannot enter the mesh regardless of which producer publishes it. Ownership is non-negotiable: a contract without an accountable product_owner_id is itself invalid, because a breach with no steward has no path to resolution. Managed this way, the contract becomes the durable interface of a spatial product — consumers plan against it, producers evolve behind it, and the version stamp guarantees the two never drift apart silently.